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

Stephen Paul Weber created

* 'master' of codeberg.org:iNPUTmice/Conversations: (273 commits)
  jingle: do not send session-terminate after failed regneg when session already was
  minor code clean up
  fix caps hash calculation for empty form fields
  fix rare concurrent modification in muc user search
  Translated using Weblate (Basque)
  Translated using Weblate (German)
  catch outdated backup exception in ImportBackupActivity
  make copy omemo fp button a show qr code button
  show unverified devices warning in contact and account details
  allow background activity start for OpenKeyChain intents
  improve logging when PGP decryption fails
  enable Java 17 language features
  add dataSync fgs type for backup import/export
  add proguard rules to fix issue in retrofit
  toggle foreground service to set correct type when gaining permissions
  bump various dependencies
  fix some linter warnings
  ignore false positive warning wrt foreground service
  update gradle and gradle plugin
  bump various dependencies
  ...

Change summary

.woodpecker.yml                                                                      |   2 
CHANGELOG.md                                                                         |  29 
build.gradle                                                                         |  40 
docs/user/migrating_to_new_device.md                                                 |  11 
fastlane/metadata/android/de-DE/changelogs/42061.txt                                 |   1 
fastlane/metadata/android/de-DE/changelogs/42062.txt                                 |   1 
fastlane/metadata/android/de-DE/changelogs/42065.txt                                 |   1 
fastlane/metadata/android/de-DE/changelogs/42068.txt                                 |   2 
fastlane/metadata/android/de-DE/changelogs/42072.txt                                 |   3 
fastlane/metadata/android/de-DE/changelogs/42074.txt                                 |   3 
fastlane/metadata/android/de-DE/changelogs/4207704.txt                               |   3 
fastlane/metadata/android/en-US/changelogs/42061.txt                                 |   1 
fastlane/metadata/android/en-US/changelogs/42062.txt                                 |   1 
fastlane/metadata/android/en-US/changelogs/42065.txt                                 |   1 
fastlane/metadata/android/en-US/changelogs/42068.txt                                 |   2 
fastlane/metadata/android/en-US/changelogs/42072.txt                                 |   3 
fastlane/metadata/android/en-US/changelogs/4207704.txt                               |   3 
fastlane/metadata/android/es-ES/changelogs/349.txt                                   |   4 
fastlane/metadata/android/es-ES/changelogs/351.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/353.txt                                   |   4 
fastlane/metadata/android/es-ES/changelogs/360.txt                                   |   1 
fastlane/metadata/android/es-ES/changelogs/362.txt                                   |   1 
fastlane/metadata/android/es-ES/changelogs/364.txt                                   |   2 
fastlane/metadata/android/es-ES/changelogs/367.txt                                   |   2 
fastlane/metadata/android/es-ES/changelogs/379.txt                                   |   1 
fastlane/metadata/android/es-ES/changelogs/381.txt                                   |   2 
fastlane/metadata/android/es-ES/changelogs/382.txt                                   |   2 
fastlane/metadata/android/es-ES/changelogs/383.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/387.txt                                   |   2 
fastlane/metadata/android/es-ES/changelogs/388.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/390.txt                                   |   1 
fastlane/metadata/android/es-ES/changelogs/393.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/394.txt                                   |   2 
fastlane/metadata/android/es-ES/changelogs/395.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/397.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/398.txt                                   |   4 
fastlane/metadata/android/es-ES/changelogs/401.txt                                   |   2 
fastlane/metadata/android/es-ES/changelogs/402.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/403.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/404.txt                                   |   1 
fastlane/metadata/android/es-ES/changelogs/405.txt                                   |   1 
fastlane/metadata/android/es-ES/changelogs/407.txt                                   |   3 
fastlane/metadata/android/es-ES/changelogs/42000.txt                                 |   4 
fastlane/metadata/android/es-ES/changelogs/42006.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42010.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42012.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42013.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42014.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42015.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42018.txt                                 |   3 
fastlane/metadata/android/es-ES/changelogs/42022.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42023.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42037.txt                                 |  11 
fastlane/metadata/android/es-ES/changelogs/42038.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42041.txt                                 |   5 
fastlane/metadata/android/es-ES/changelogs/42042.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42043.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42044.txt                                 |   3 
fastlane/metadata/android/es-ES/changelogs/42046.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42047.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42050.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42059.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42060.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42061.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42062.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42065.txt                                 |   1 
fastlane/metadata/android/es-ES/changelogs/42068.txt                                 |   2 
fastlane/metadata/android/es-ES/changelogs/42072.txt                                 |   3 
fastlane/metadata/android/es-ES/changelogs/4207704.txt                               |   3 
fastlane/metadata/android/gl-ES/changelogs/349.txt                                   |   4 
fastlane/metadata/android/gl-ES/changelogs/351.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/353.txt                                   |   4 
fastlane/metadata/android/gl-ES/changelogs/360.txt                                   |   1 
fastlane/metadata/android/gl-ES/changelogs/362.txt                                   |   1 
fastlane/metadata/android/gl-ES/changelogs/364.txt                                   |   2 
fastlane/metadata/android/gl-ES/changelogs/367.txt                                   |   2 
fastlane/metadata/android/gl-ES/changelogs/379.txt                                   |   1 
fastlane/metadata/android/gl-ES/changelogs/381.txt                                   |   2 
fastlane/metadata/android/gl-ES/changelogs/382.txt                                   |   2 
fastlane/metadata/android/gl-ES/changelogs/383.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/387.txt                                   |   2 
fastlane/metadata/android/gl-ES/changelogs/388.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/390.txt                                   |   1 
fastlane/metadata/android/gl-ES/changelogs/393.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/394.txt                                   |   2 
fastlane/metadata/android/gl-ES/changelogs/395.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/397.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/398.txt                                   |   4 
fastlane/metadata/android/gl-ES/changelogs/401.txt                                   |   2 
fastlane/metadata/android/gl-ES/changelogs/402.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/403.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/404.txt                                   |   1 
fastlane/metadata/android/gl-ES/changelogs/405.txt                                   |   1 
fastlane/metadata/android/gl-ES/changelogs/407.txt                                   |   3 
fastlane/metadata/android/gl-ES/changelogs/42000.txt                                 |   4 
fastlane/metadata/android/gl-ES/changelogs/42006.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42010.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42012.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42013.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42014.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42015.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42018.txt                                 |   3 
fastlane/metadata/android/gl-ES/changelogs/42022.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42023.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42037.txt                                 |  11 
fastlane/metadata/android/gl-ES/changelogs/42038.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42041.txt                                 |   5 
fastlane/metadata/android/gl-ES/changelogs/42042.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42043.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42044.txt                                 |   3 
fastlane/metadata/android/gl-ES/changelogs/42046.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42047.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42050.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42059.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42060.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42061.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42062.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42065.txt                                 |   1 
fastlane/metadata/android/gl-ES/changelogs/42068.txt                                 |   2 
fastlane/metadata/android/gl-ES/changelogs/42072.txt                                 |   3 
fastlane/metadata/android/gl-ES/changelogs/42074.txt                                 |   3 
fastlane/metadata/android/it-IT/changelogs/349.txt                                   |   4 
fastlane/metadata/android/it-IT/changelogs/351.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/353.txt                                   |   4 
fastlane/metadata/android/it-IT/changelogs/360.txt                                   |   1 
fastlane/metadata/android/it-IT/changelogs/362.txt                                   |   1 
fastlane/metadata/android/it-IT/changelogs/364.txt                                   |   2 
fastlane/metadata/android/it-IT/changelogs/367.txt                                   |   2 
fastlane/metadata/android/it-IT/changelogs/379.txt                                   |   1 
fastlane/metadata/android/it-IT/changelogs/381.txt                                   |   2 
fastlane/metadata/android/it-IT/changelogs/382.txt                                   |   2 
fastlane/metadata/android/it-IT/changelogs/383.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/387.txt                                   |   2 
fastlane/metadata/android/it-IT/changelogs/388.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/390.txt                                   |   1 
fastlane/metadata/android/it-IT/changelogs/393.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/394.txt                                   |   2 
fastlane/metadata/android/it-IT/changelogs/395.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/397.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/398.txt                                   |   4 
fastlane/metadata/android/it-IT/changelogs/401.txt                                   |   2 
fastlane/metadata/android/it-IT/changelogs/402.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/403.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/404.txt                                   |   1 
fastlane/metadata/android/it-IT/changelogs/405.txt                                   |   1 
fastlane/metadata/android/it-IT/changelogs/407.txt                                   |   3 
fastlane/metadata/android/it-IT/changelogs/42000.txt                                 |   4 
fastlane/metadata/android/it-IT/changelogs/42006.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42010.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42012.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42013.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42014.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42015.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42018.txt                                 |   3 
fastlane/metadata/android/it-IT/changelogs/42022.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42023.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42037.txt                                 |  11 
fastlane/metadata/android/it-IT/changelogs/42038.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42041.txt                                 |   5 
fastlane/metadata/android/it-IT/changelogs/42042.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42043.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42044.txt                                 |   3 
fastlane/metadata/android/it-IT/changelogs/42046.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42047.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42050.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42059.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42060.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42061.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42062.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42065.txt                                 |   1 
fastlane/metadata/android/it-IT/changelogs/42068.txt                                 |   2 
fastlane/metadata/android/it-IT/changelogs/42072.txt                                 |   3 
fastlane/metadata/android/it-IT/changelogs/42074.txt                                 |   3 
fastlane/metadata/android/it-IT/changelogs/4207704.txt                               |   3 
fastlane/metadata/android/ro/changelogs/349.txt                                      |   4 
fastlane/metadata/android/ro/changelogs/351.txt                                      |   3 
fastlane/metadata/android/ro/changelogs/353.txt                                      |   4 
fastlane/metadata/android/ro/changelogs/360.txt                                      |   1 
fastlane/metadata/android/ro/changelogs/362.txt                                      |   1 
fastlane/metadata/android/ro/changelogs/364.txt                                      |   2 
fastlane/metadata/android/ro/changelogs/367.txt                                      |   2 
fastlane/metadata/android/ro/changelogs/379.txt                                      |   1 
fastlane/metadata/android/ro/changelogs/381.txt                                      |   2 
fastlane/metadata/android/ro/changelogs/382.txt                                      |   2 
fastlane/metadata/android/ro/changelogs/383.txt                                      |   3 
fastlane/metadata/android/ro/changelogs/387.txt                                      |   2 
fastlane/metadata/android/ro/changelogs/388.txt                                      |   3 
fastlane/metadata/android/ro/changelogs/390.txt                                      |   1 
fastlane/metadata/android/ro/changelogs/393.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/349.txt                                      |   4 
fastlane/metadata/android/uk/changelogs/351.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/353.txt                                      |   4 
fastlane/metadata/android/uk/changelogs/360.txt                                      |   1 
fastlane/metadata/android/uk/changelogs/362.txt                                      |   1 
fastlane/metadata/android/uk/changelogs/364.txt                                      |   2 
fastlane/metadata/android/uk/changelogs/367.txt                                      |   2 
fastlane/metadata/android/uk/changelogs/379.txt                                      |   1 
fastlane/metadata/android/uk/changelogs/381.txt                                      |   2 
fastlane/metadata/android/uk/changelogs/382.txt                                      |   2 
fastlane/metadata/android/uk/changelogs/383.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/387.txt                                      |   2 
fastlane/metadata/android/uk/changelogs/388.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/390.txt                                      |   1 
fastlane/metadata/android/uk/changelogs/393.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/394.txt                                      |   2 
fastlane/metadata/android/uk/changelogs/395.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/397.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/398.txt                                      |   4 
fastlane/metadata/android/uk/changelogs/401.txt                                      |   2 
fastlane/metadata/android/uk/changelogs/402.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/403.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/404.txt                                      |   1 
fastlane/metadata/android/uk/changelogs/405.txt                                      |   1 
fastlane/metadata/android/uk/changelogs/407.txt                                      |   3 
fastlane/metadata/android/uk/changelogs/42000.txt                                    |   4 
fastlane/metadata/android/uk/changelogs/42006.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42010.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42012.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42013.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42014.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42015.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42018.txt                                    |   3 
fastlane/metadata/android/uk/changelogs/42022.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42023.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42037.txt                                    |  11 
fastlane/metadata/android/uk/changelogs/42038.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42041.txt                                    |   5 
fastlane/metadata/android/uk/changelogs/42042.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42043.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42044.txt                                    |   3 
fastlane/metadata/android/uk/changelogs/42046.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42047.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42050.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42059.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42060.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42061.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42062.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42065.txt                                    |   1 
fastlane/metadata/android/uk/changelogs/42068.txt                                    |   2 
fastlane/metadata/android/uk/changelogs/42072.txt                                    |   3 
fastlane/metadata/android/uk/changelogs/4207704.txt                                  |   3 
fastlane/metadata/android/zh-CN/changelogs/349.txt                                   |   4 
fastlane/metadata/android/zh-CN/changelogs/351.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/353.txt                                   |   4 
fastlane/metadata/android/zh-CN/changelogs/360.txt                                   |   1 
fastlane/metadata/android/zh-CN/changelogs/362.txt                                   |   1 
fastlane/metadata/android/zh-CN/changelogs/364.txt                                   |   2 
fastlane/metadata/android/zh-CN/changelogs/367.txt                                   |   2 
fastlane/metadata/android/zh-CN/changelogs/379.txt                                   |   1 
fastlane/metadata/android/zh-CN/changelogs/381.txt                                   |   2 
fastlane/metadata/android/zh-CN/changelogs/382.txt                                   |   2 
fastlane/metadata/android/zh-CN/changelogs/383.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/387.txt                                   |   2 
fastlane/metadata/android/zh-CN/changelogs/388.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/390.txt                                   |   1 
fastlane/metadata/android/zh-CN/changelogs/393.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/394.txt                                   |   2 
fastlane/metadata/android/zh-CN/changelogs/395.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/397.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/398.txt                                   |   4 
fastlane/metadata/android/zh-CN/changelogs/401.txt                                   |   2 
fastlane/metadata/android/zh-CN/changelogs/402.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/403.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/404.txt                                   |   1 
fastlane/metadata/android/zh-CN/changelogs/405.txt                                   |   1 
fastlane/metadata/android/zh-CN/changelogs/407.txt                                   |   3 
fastlane/metadata/android/zh-CN/changelogs/42000.txt                                 |   4 
fastlane/metadata/android/zh-CN/changelogs/42006.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42010.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42012.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42013.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42014.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42015.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42018.txt                                 |   3 
fastlane/metadata/android/zh-CN/changelogs/42022.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42023.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42037.txt                                 |  11 
fastlane/metadata/android/zh-CN/changelogs/42038.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42041.txt                                 |   5 
fastlane/metadata/android/zh-CN/changelogs/42042.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42043.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42044.txt                                 |   3 
fastlane/metadata/android/zh-CN/changelogs/42046.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42047.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42050.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42059.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42060.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42061.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42062.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42065.txt                                 |   1 
fastlane/metadata/android/zh-CN/changelogs/42068.txt                                 |   2 
fastlane/metadata/android/zh-CN/changelogs/42072.txt                                 |   3 
fastlane/metadata/android/zh-CN/changelogs/42074.txt                                 |   3 
fastlane/metadata/android/zh-CN/changelogs/4207704.txt                               |   3 
gradle.properties                                                                    |   2 
gradle/wrapper/gradle-wrapper.properties                                             |   4 
proguard-rules.pro                                                                   |  14 
src/cheogram/res/values/themes.xml                                                   |   2 
src/conversations/fastlane/metadata/android/da-DK/short_description.txt              |   0 
src/conversations/fastlane/metadata/android/de-DE/full_description.txt               |   0 
src/conversations/fastlane/metadata/android/de-DE/short_description.txt              |   0 
src/conversations/fastlane/metadata/android/en-US/full_description.txt               |   0 
src/conversations/fastlane/metadata/android/en-US/images/icon.png                    |   0 
src/conversations/fastlane/metadata/android/en-US/short_description.txt              |   0 
src/conversations/fastlane/metadata/android/es-ES/full_description.txt               |  39 
src/conversations/fastlane/metadata/android/es-ES/short_description.txt              |   1 
src/conversations/fastlane/metadata/android/gl-ES/full_description.txt               |   0 
src/conversations/fastlane/metadata/android/gl-ES/short_description.txt              |   0 
src/conversations/fastlane/metadata/android/it-IT/full_description.txt               |   0 
src/conversations/fastlane/metadata/android/it-IT/short_description.txt              |   0 
src/conversations/fastlane/metadata/android/pl-PL/full_description.txt               |   0 
src/conversations/fastlane/metadata/android/pl-PL/short_description.txt              |   0 
src/conversations/fastlane/metadata/android/ro/full_description.txt                  |  38 
src/conversations/fastlane/metadata/android/ro/short_description.txt                 |   0 
src/conversations/fastlane/metadata/android/sq/full_description.txt                  |   0 
src/conversations/fastlane/metadata/android/sq/short_description.txt                 |   0 
src/conversations/fastlane/metadata/android/sv-SE/full_description.txt               |   0 
src/conversations/fastlane/metadata/android/sv-SE/short_description.txt              |   0 
src/conversations/fastlane/metadata/android/uk/full_description.txt                  |  39 
src/conversations/fastlane/metadata/android/uk/short_description.txt                 |   1 
src/conversations/fastlane/metadata/android/zh-CN/full_description.txt               |  39 
src/conversations/fastlane/metadata/android/zh-CN/short_description.txt              |   1 
src/conversations/fastlane/metadata/android/zh-TW/full_description.txt               |   0 
src/conversations/fastlane/metadata/android/zh-TW/short_description.txt              |   0 
src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java      | 300 
src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java           |   3 
src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java          |   1 
src/conversations/res/drawable/ic_launcher_foreground.xml                            |  31 
src/conversations/res/drawable/ic_launcher_monochrome.xml                            |  13 
src/conversations/res/values-el/strings.xml                                          |   2 
src/conversations/res/values-sk/strings.xml                                          |   2 
src/conversations/res/values-tr-rTR/strings.xml                                      |   4 
src/conversations/res/values-uk/strings.xml                                          |  18 
src/conversations/res/values-zh-rCN/strings.xml                                      |  24 
src/main/AndroidManifest.xml                                                         |  81 
src/main/java/de/gultsch/minidns/AndroidDNSClient.java                               | 221 
src/main/java/de/gultsch/minidns/DNSServer.java                                      | 104 
src/main/java/de/gultsch/minidns/DNSSocket.java                                      | 199 
src/main/java/de/gultsch/minidns/NetworkDataSource.java                              | 160 
src/main/java/de/gultsch/minidns/Transport.java                                      |  23 
src/main/java/eu/siacs/conversations/Config.java                                     |   4 
src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java                |   4 
src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java              |   6 
src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java           |   4 
src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java                 |  10 
src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java        |   9 
src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java                  |   2 
src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java                |   2 
src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java                |   2 
src/main/java/eu/siacs/conversations/entities/Account.java                           |  21 
src/main/java/eu/siacs/conversations/entities/MucOptions.java                        |   5 
src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java            |  30 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java                 |   4 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                       |  43 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java                    |   9 
src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java |   5 
src/main/java/eu/siacs/conversations/services/AvatarService.java                     |   2 
src/main/java/eu/siacs/conversations/services/NotificationService.java               | 142 
src/main/java/eu/siacs/conversations/services/ShortcutService.java                   |  20 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java             | 734 
src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java                   |   5 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java               |   2 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java                  |   8 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java                    |  30 
src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java                   |   7 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java                     |  27 
src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java                        |  39 
src/main/java/eu/siacs/conversations/ui/OmemoActivity.java                           |   1 
src/main/java/eu/siacs/conversations/ui/RecordingActivity.java                       |  36 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java                        |   1 
src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java                        |   2 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java               |  16 
src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java                      |  95 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                            |  52 
src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java                  |   1 
src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java                    |   2 
src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java             |   2 
src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java                     |   3 
src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java                  |   3 
src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java         |   3 
src/main/java/eu/siacs/conversations/utils/AccountUtils.java                         |   2 
src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java                     |  27 
src/main/java/eu/siacs/conversations/utils/Compatibility.java                        |  24 
src/main/java/eu/siacs/conversations/utils/FileUtils.java                            |   4 
src/main/java/eu/siacs/conversations/utils/IP.java                                   |  12 
src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java             |   2 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                            |  68 
src/main/java/eu/siacs/conversations/utils/PermissionUtils.java                      |  20 
src/main/java/eu/siacs/conversations/utils/Resolver.java                             |  89 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                             |   2 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                        |  63 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java        |  49 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java            | 124 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java                  |  40 
src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java             |  54 
src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java                  |   5 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java    |   8 
src/main/res/drawable-v24/ic_launcher_background.xml                                 |  33 
src/main/res/drawable/ic_logout_white_24dp.xml                                       |   5 
src/main/res/drawable/ic_play_lesson_black_24.xml                                    |   6 
src/main/res/drawable/ic_play_lesson_white_48dp.xml                                  |   6 
src/main/res/drawable/ic_qr_code_black_24dp.xml                                      |  40 
src/main/res/drawable/ic_qr_code_white_24dp.xml                                      |  40 
src/main/res/layout/activity_contact_details.xml                                     |  13 
src/main/res/layout/activity_edit_account.xml                                        | 225 
src/main/res/mipmap-anydpi-v26/new_launcher.xml                                      |   5 
src/main/res/mipmap-anydpi-v26/new_launcher_round.xml                                |   3 
src/main/res/mipmap-hdpi/ic_launcher_background.png                                  |   0 
src/main/res/mipmap-mdpi/ic_launcher_background.png                                  |   0 
src/main/res/mipmap-xhdpi/ic_launcher_background.png                                 |   0 
src/main/res/mipmap-xxhdpi/ic_launcher_background.png                                |   0 
src/main/res/mipmap-xxxhdpi/ic_launcher_background.png                               |   0 
src/main/res/values-ar/strings.xml                                                   |  23 
src/main/res/values-ca/strings.xml                                                   |  47 
src/main/res/values-cs/strings.xml                                                   |   2 
src/main/res/values-de/strings.xml                                                   |   6 
src/main/res/values-es/strings.xml                                                   |   6 
src/main/res/values-eu/strings.xml                                                   |   4 
src/main/res/values-fr/strings.xml                                                   |  20 
src/main/res/values-gl/strings.xml                                                   |  10 
src/main/res/values-it/strings.xml                                                   |   6 
src/main/res/values-pl/strings.xml                                                   |   4 
src/main/res/values-ro-rRO/strings.xml                                               |   4 
src/main/res/values-ru/strings.xml                                                   |  87 
src/main/res/values-tr-rTR/strings.xml                                               | 132 
src/main/res/values-uk/strings.xml                                                   | 497 
src/main/res/values-zh-rCN/strings.xml                                               | 887 
src/main/res/values/attrs.xml                                                        |   3 
src/main/res/values/strings.xml                                                      |  11 
src/main/res/values/themes.xml                                                       |   6 
src/main/res/xml/data_extraction_rules.xml                                           |  11 
src/main/res/xml/locales_config.xml                                                  |  44 
src/quicksy/fastlane/metadata/android/de-DE/full_description.txt                     |  14 
src/quicksy/fastlane/metadata/android/de-DE/short_description.txt                    |   1 
src/quicksy/fastlane/metadata/android/en-US/full_description.txt                     |  14 
src/quicksy/fastlane/metadata/android/en-US/images/icon.png                          |   0 
src/quicksy/fastlane/metadata/android/en-US/short_description.txt                    |   1 
src/quicksy/fastlane/metadata/android/gl-ES/full_description.txt                     |  14 
src/quicksy/fastlane/metadata/android/gl-ES/short_description.txt                    |   1 
src/quicksy/fastlane/metadata/android/it-IT/full_description.txt                     |  14 
src/quicksy/fastlane/metadata/android/it-IT/short_description.txt                    |   1 
src/quicksy/fastlane/metadata/android/ro/full_description.txt                        |  14 
src/quicksy/fastlane/metadata/android/ro/short_description.txt                       |   1 
src/quicksy/fastlane/metadata/android/uk/full_description.txt                        |  14 
src/quicksy/fastlane/metadata/android/uk/short_description.txt                       |   1 
src/quicksy/fastlane/metadata/android/zh-CN/full_description.txt                     |  14 
src/quicksy/fastlane/metadata/android/zh-CN/short_description.txt                    |   1 
src/quicksy/res/drawable/ic_launcher_foreground.xml                                  |   7 
src/quicksy/res/drawable/ic_launcher_monochrome.xml                                  |  10 
src/quicksy/res/mipmap-anydpi-v26/new_launcher.xml                                   |   5 
src/quicksy/res/mipmap-anydpi-v26/new_launcher_round.xml                             |   5 
src/quicksy/res/values-ru/strings.xml                                                |   8 
src/quicksy/res/values-tr-rTR/strings.xml                                            |   4 
src/quicksy/res/values-uk/strings.xml                                                |  12 
src/quicksy/res/values-zh-rCN/strings.xml                                            |  10 
455 files changed, 4,758 insertions(+), 1,693 deletions(-)

Detailed changes

.woodpecker.yml 🔗

@@ -1,4 +1,4 @@
-pipeline:
+steps:
     build:
         image: codeberg.org/freeyourgadget/android-fdroid-tools:latest
         commands:

CHANGELOG.md 🔗

@@ -1,5 +1,34 @@
 # Changelog
 
+### Version 2.12.12
+
+* Support Private DNS (DNS over TLS)
+* Support themed launcher icon
+* Fix rare permission issue when sharing files on Android 11+
+
+### Version 2.12.11
+
+* Bump libwebrtc dependency to M117 and bump libvpx
+* Go back to AAC for voice messages
+* Support per app language settings
+
+### Version 2.12.10
+
+* support per conversation notification settings
+* use opus for voice messages on Android 10
+
+### Version 2.12.9
+
+* Introduce new backup file format
+
+### Version 2.12.8
+
+* Disable opening backup files (.ceb) from file manager
+
+### Version 2.12.7
+
+* Remove channel discovery feature from Google Play version
+
 ### Version 2.12.6
 
 * Fix 'q' falsely being recognized as cyrillic

build.gradle 🔗

@@ -6,7 +6,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.4.2'
+        classpath 'com.android.tools.build:gradle:8.2.0-rc01'
     }
 }
 
@@ -37,17 +37,6 @@ configurations {
 }
 
 dependencies {
-   constraints {
-       implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
-           because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
-       }
-       implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
-           because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
-       }
-   }
-
-    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
-
     androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
     androidTestImplementation 'junit:junit:4.13.2'
     androidTestImplementation 'androidx.test:runner:1.3.0'
@@ -56,9 +45,11 @@ dependencies {
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
 
     implementation "androidx.core:core:1.10.1"
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
+
     implementation 'androidx.viewpager:viewpager:1.0.0'
 
-    playstoreImplementation('com.google.firebase:firebase-messaging:23.1.2') {
+    playstoreImplementation('com.google.firebase:firebase-messaging:23.3.0') {
         exclude group: 'com.google.firebase', module: 'firebase-core'
         exclude group: 'com.google.firebase', module: 'firebase-analytics'
         exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@@ -73,10 +64,10 @@ dependencies {
     implementation 'androidx.exifinterface:exifinterface:1.3.6'
     implementation 'androidx.cardview:cardview:1.0.0'
     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
-    implementation 'com.google.android.material:material:1.8.0'
+    implementation 'com.google.android.material:material:1.10.0'
 
-    implementation "androidx.emoji2:emoji2:1.2.0"
-    freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0"
+    implementation "androidx.emoji2:emoji2:1.4.0"
+    freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0"
 
     implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
     //zxing stopped supporting Java 7 so we have to stick with 3.3.3
@@ -104,8 +95,8 @@ dependencies {
     implementation "com.squareup.retrofit2:converter-gson:2.9.0"
     implementation "com.squareup.okhttp3:okhttp:4.10.0"
 
-    implementation 'com.google.guava:guava:31.1-android'
-    implementation 'io.michaelrocks:libphonenumber-android:8.12.49'
+    implementation 'com.google.guava:guava:32.1.3-android'
+    implementation 'io.michaelrocks:libphonenumber-android:8.13.17'
     implementation 'io.github.nishkarsh:android-permissions:2.1.6'
     implementation 'androidx.recyclerview:recyclerview:1.1.0'
     implementation 'androidx.documentfile:documentfile:1.0.1'
@@ -131,11 +122,11 @@ ext {
 
 android {
     namespace 'eu.siacs.conversations'
-    compileSdkVersion 33
+    compileSdkVersion 34
 
     defaultConfig {
         minSdkVersion 21
-        targetSdkVersion 33
+        targetSdkVersion 34
         versionCode 42025 + tags.size()
         versionName grgit.describe(always: true)
         applicationId "eu.siacs.conversations"
@@ -163,8 +154,8 @@ android {
 
     compileOptions {
         coreLibraryDesugaringEnabled true
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
     }
 
     flavorDimensions("mode", "distribution")
@@ -296,7 +287,10 @@ android {
         }
     }
     lint {
-        disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource', 'ExtraTranslation'
+        disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource'
+    }
+    buildFeatures {
+        buildConfig true
     }
 
     android.applicationVariants.all { variant ->

docs/user/migrating_to_new_device.md 🔗

@@ -22,12 +22,11 @@ This tutorial explains how you can transfer your Conversations data from an old
 ## 3. Import the backup (new device)
 1. Install Conversations on your new device.
 2. Open Conversations for the first time.
-3. Tap on "Use other server"
-4. Tap on the three dot menu in the upper right corner and tap on "Import backup"
-5. If your backup files are not listed, tap on the cloud symbol in the upper right corner to choose the files from the where you saved them.
-6. Enter your account password to decrypt the backup.
-7. Remember to activate your account (head back to "manage accounts", see step 1.2).
-8. Check if chats work.
+3. Tap on the three dot menu in the upper right corner and tap on "Import backup"
+4. If your backup files are not listed, tap on the cloud symbol in the upper right corner to choose the files from where you saved them.
+5. Enter your account password to decrypt the backup.
+6. Remember to activate your account (head back to "manage accounts", see step 1.2).
+7. Check if chats work.
 
 Once confirmed that the new device is running fine you can just uninstall the app from the old device.
 

fastlane/metadata/android/es-ES/changelogs/349.txt 🔗

@@ -0,0 +1,4 @@
+* Introducir configuración experta para realizar el descubrimiento de canales en el servidor local en lugar de search.jabber.network
+* Habilitar las marcas de verificación de entrega por defecto y eliminar la configuración
+* Habilitar «Enviar botón indica estado» por defecto y eliminar la configuración
+* Mover los ajustes de copia de seguridad y servicio en primer plano a la pantalla principal

fastlane/metadata/android/es-ES/changelogs/353.txt 🔗

@@ -0,0 +1,4 @@
+* Permitir a los usuarios establecer su propio apodo
+* reanudar la descarga de archivos encriptados OMEMO
+* Los canales ahora usan '#' como símbolo en el avatar
+* Quicksy utiliza «siempre» como cifrado OMEMO por defecto (oculta el icono del candado)

fastlane/metadata/android/es-ES/changelogs/383.txt 🔗

@@ -0,0 +1,3 @@
+* Mover el icono de llamada a la izquierda para mantener otros iconos de la barra de herramientas en un lugar coherente.
+* Mostrar la duración de la llamada durante las llamadas de audio
+* Desempate en las llamadas A/V (dos personas que se llaman al mismo tiempo)

fastlane/metadata/android/es-ES/changelogs/388.txt 🔗

@@ -0,0 +1,3 @@
+* Reducir el eco durante las llamadas en algunos dispositivos
+* Arreglar el inicio de sesión cuando las contraseñas contienen caracteres especiales
+* Reproducir tonos de marcado y ocupado en el altavoz durante las videollamadas

fastlane/metadata/android/es-ES/changelogs/394.txt 🔗

@@ -0,0 +1,2 @@
+* Se ha corregido el problema de las notificaciones que no aparecían en determinadas circunstancias.
+* Se han solucionado problemas de compatibilidad y bloqueos relacionados con las llamadas A/V

fastlane/metadata/android/es-ES/changelogs/398.txt 🔗

@@ -0,0 +1,4 @@
+* Buscar conversaciones individuales
+* Notificar al usuario si falla la entrega del mensaje
+* Recordar los nombres de usuario (nicks) de los usuarios de Quicksy en los reinicios
+* Añadir el botón para iniciar Orbot (Tor) desde la notificación si es necesario

fastlane/metadata/android/es-ES/changelogs/403.txt 🔗

@@ -0,0 +1,3 @@
+* Corregidos problemas de conectividad cuando diferentes cuentas utilizaban diferentes mecanismos SCRAM.
+* Añadir soporte para SCRAM-SHA-512
+* Permitir la transferencia de archivos P2P (Jingle) con autocontacto

fastlane/metadata/android/es-ES/changelogs/42000.txt 🔗

@@ -0,0 +1,4 @@
+* Posibilidad de seleccionar el tono de la llamada entrante
+* Corrección de la identificación de claves OpenPGP para OpenKeychain 5.6+.
+* Verificación correcta de los certificados TLS punycode
+* Mejora de la estabilidad del establecimiento de sesiones RTP (llamadas)

fastlane/metadata/android/es-ES/changelogs/42018.txt 🔗

@@ -0,0 +1,3 @@
+* Mostrar barras negras cuando el vídeo remoto no coincide con la relación del aspecto de la pantalla.
+* Mejorar el rendimiento de la búsqueda
+* Añadir configuración para evitar capturas de pantalla

fastlane/metadata/android/es-ES/changelogs/42037.txt 🔗

@@ -0,0 +1,11 @@
+Versión 2.10.9
+* Pedir permisos Bluetooth al hacer llamadas A/V (Puede rechazar esto si no utiliza auriculares Bluetooth).
+* Corrección de error al llamar a Movim
+* Corregir avatar incorrecto que se muestra para los chats de grupo
+* Preguntar siempre por las optimizaciones de batería
+* Establecer sólo local bandera en 'x cuentas conectadas' notificaciones
+* Corrección de la interacción con Google Maps Share Location Plugin
+* Eliminar nota a pie de página con respecto a la cuota del servidor
+* Almacenar archivos en la ubicación adecuada para Android 11
+* Intento de reconectar llamada tras cambio de red
+* Mostrar el JID de la persona que llama y el JID de la cuenta en la pantalla de llamada entrante

fastlane/metadata/android/es-ES/changelogs/42041.txt 🔗

@@ -0,0 +1,5 @@
+* Implementación del perfil SASL extensible, Bind 2.0 y Fast para reconexiones más rápidas.
+* Implementación de Channel Binding
+* Añadir la posibilidad de cambiar de llamada de audio a videollamada
+* Añadir la posibilidad de eliminar el propio avatar
+* Notificación de llamadas perdidas

fastlane/metadata/android/gl-ES/changelogs/349.txt 🔗

@@ -0,0 +1,4 @@
+* Introdución do axuste de experta para realizar o descubrimento de canle no servidor local e non buscar en search.jabber.network
+* Activadas as marcas de comprobación de entrega por defecto e eliminación do axuste
+* Activar por defecto 'O botón enviar indica estado' e eliminar o axuste
+* Mover os axustes Copia de Apoio e Servizo en primeiro plano á pantalla principal

fastlane/metadata/android/gl-ES/changelogs/383.txt 🔗

@@ -0,0 +1,3 @@
+* Move call icon to the left in order to keep other toolbar icons in a consistent place
+* Show call duration during audio calls
+* Tie breaking for A/V calls (the same two people calling each other at the same time)

fastlane/metadata/android/gl-ES/changelogs/398.txt 🔗

@@ -0,0 +1,4 @@
+* Search individual conversations
+* Notify user if message delivery fails
+* Remember display names (nicks) from Quicksy users across restarts
+* Add button to start Orbot (Tor) from notification if necessary

fastlane/metadata/android/gl-ES/changelogs/42037.txt 🔗

@@ -0,0 +1,11 @@
+Version 2.10.9
+* Ask for Bluetooth permissions when making A/V calls (You can reject this if you don’t use Bluetooth headsets)
+* Fix bug when calling Movim
+* Fix wrong avatar being shown for group chats
+* Always ask for battery optimizations opt-out
+* Set local only flag on 'x connected accounts' notifications
+* Fix interaction with Google Maps Share Location Plugin
+* Remove footnote with regards to server fee
+* Store files in location appropriate for Android 11
+* Attempt to reconnect call after network switch
+* Show caller JID and account JID in incoming call screen

fastlane/metadata/android/gl-ES/changelogs/42041.txt 🔗

@@ -0,0 +1,5 @@
+* Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects
+* Implement Channel Binding
+* Add ability to switch from audio call to video call
+* Add ability to delete own avatar
+* Add notification for missed calls

fastlane/metadata/android/it-IT/changelogs/349.txt 🔗

@@ -0,0 +1,4 @@
+* Introdotta l'impostazione per esperti per eseguire la ricerca dei canali sul server locale invece che su search.jabber.network
+* Attivati i segni di spunta per la consegna in modo predefinito e rimossa l'impostazione
+* Attivato "Il pulsante di invio indica lo stato" in modo predefinito e rimossa l'impostazione
+* Spostate le impostazioni del servizio di backup e di primo piano nella schermata principale

fastlane/metadata/android/it-IT/changelogs/353.txt 🔗

@@ -0,0 +1,4 @@
+* Consente agli utenti di impostare il proprio nick name
+* Riprende il download dei file criptati OMEMO
+* I canali ora usano '#' come simbolo nell'avatar.
+* Quicksy imposta "sempre" come crittografia OMEMO in modo predefinito (nasconde l'icona del lucchetto)

fastlane/metadata/android/it-IT/changelogs/383.txt 🔗

@@ -0,0 +1,3 @@
+* Spostata l'icona della chiamata a sinistra per mantenere le altre icone della barra degli strumenti in una posizione coerente
+* Mostra la durata della chiamata durante le chiamate audio
+* Interruzione della parità per le chiamate A/V (due persone che si chiamano contemporaneamente)

fastlane/metadata/android/it-IT/changelogs/388.txt 🔗

@@ -0,0 +1,3 @@
+* Riduzione dell'eco durante le chiamate su alcuni dispositivi
+* Corretto l'accesso quando le password contengono caratteri speciali
+* Riproduzione dei toni di chiamata e di occupato sull'altoparlante durante le videochiamate

fastlane/metadata/android/it-IT/changelogs/398.txt 🔗

@@ -0,0 +1,4 @@
+* Ricerca di conversazioni individuali
+* Notifica all'utente se la consegna del messaggio fallisce
+* Ricorda i nomi visualizzati (nick) degli utenti di Quicksy durante i vari riavvii
+* Aggiunto un pulsante per avviare Orbot (Tor) dalla notifica, se necessario

fastlane/metadata/android/it-IT/changelogs/403.txt 🔗

@@ -0,0 +1,3 @@
+* Corretti i problemi di connettività quando profili diversi usavano meccanismi SCRAM diversi
+* Aggiunto il supporto per SCRAM-SHA-512
+* Consente il trasferimento di file P2P (Jingle) con l'auto contatto

fastlane/metadata/android/it-IT/changelogs/407.txt 🔗

@@ -0,0 +1,3 @@
+* Mostra il pulsante di chiamata per i contatti offline se hanno precedentemente annunciato il supporto
+* Il pulsante Indietro non termina più la chiamata quando questa è connessa
+* Correzioni di errori

fastlane/metadata/android/it-IT/changelogs/42000.txt 🔗

@@ -0,0 +1,4 @@
+* Possibilità di selezionare la suoneria delle chiamate in arrivo
+* Correzione del rilevamento dell'id della chiave OpenPGP per OpenKeychain 5.6+
+* Verifica corretta dei certificati TLS con codice punycode
+* Miglioramento della stabilità della creazione di sessioni RTP (chiamate)

fastlane/metadata/android/it-IT/changelogs/42037.txt 🔗

@@ -0,0 +1,11 @@
+2.10.9
+* Permessi Bluetooth per chiamate A/V (puoi rifiutare)
+* Correto bug chiamando Movim
+* Corretti avatar nelle chat di gruppo
+* Chiedi sempre opt-out di ottimizzazione batteria
+* Flag solo locale su notifiche "x profili connessi"
+* Corretta interazione con plugin di condivisione posizione Google Maps
+* Rimossa nota del costo del server
+* Archivia i file nel posto giusto su Android 11
+* Ricollega chiamata dopo il cambio di rete
+* Mostra JID chiamante e JID profilo nelle chiamate in entrata

fastlane/metadata/android/it-IT/changelogs/42041.txt 🔗

@@ -0,0 +1,5 @@
+* Implementato il profilo SASL estensibile, Bind 2.0 e Fast per riconnettersi più velocemente
+* Implementato il Channel Binding
+* Aggiunta la possibilità di passare da una chiamata audio a una videochiamata
+* Aggiunta la possibilità di cancellare il proprio avatar
+* Aggiunta la notifica per le chiamate perse

fastlane/metadata/android/ro/changelogs/349.txt 🔗

@@ -0,0 +1,4 @@
+* Introducerea setărilor pentru experți pentru a efectua descoperirea canalelor pe serverul local în loc de search.jabber.network
+* Activarea marcajelor de verificare a livrării în mod implicit și eliminarea setării
+* Activarea "Butonul de trimitere indică starea" în mod implicit și eliminarea setării
+*Mutarea setărilor Serviciului de rezervă și ale Serviciului de prim-plan în ecranul principal

fastlane/metadata/android/ro/changelogs/353.txt 🔗

@@ -0,0 +1,4 @@
+* utilizatorii pot să își seteze propria poreclă
+* continuarea descărcării de fișiere criptate OMEMO
+* Canalele folosesc '#' ca simbol în avatar
+* Quicksy folosește 'mereu' ca și criptare implicită OMEMO (ascunde iconița lacăt)

fastlane/metadata/android/ro/changelogs/383.txt 🔗

@@ -0,0 +1,3 @@
+* Mutarea iconiței de apel către stânga pentru a ține celelalte iconițe din bara de instrumente într-un loc consistent
+* Afișarea durației apelurilor în timpul apelurilor audio
+* Ruperea egalității pentru apeluri audio/video (aceleași două persoane care se sună între ele în același timp)

fastlane/metadata/android/ro/changelogs/388.txt 🔗

@@ -0,0 +1,3 @@
+* Reducerea ecoului în timpul apelurilor pe unele dispozitive
+* Repararea logării când parolele conțin caractere speciale
+* Redarea tonurilor de apel și ocupat pe difuzor în timpul apelurilor video

fastlane/metadata/android/ro/changelogs/393.txt 🔗

@@ -0,0 +1,3 @@
+* Afișarea butonului de ajutor dacă apelul audio/video eșuează
+* Repararea unor crash-uri enervante
+* Repararea conexiunilor Jingle (transfer fișiere + apeluri) cu JID-uri goale

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

@@ -0,0 +1,4 @@
+* Додано Експертні налаштування для пошуку каналів на локальному сервері замість search.jabber.network
+* Позначки про доставку увімкнено за замовчуванням, а налаштування видалено
+* «Кнопка надсилання показує стан» увімкнено за замовчуванням, а налаштування видалено
+* Налаштування резервного копіювання і процесу на передньому плані перенесено на основний екран

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

@@ -0,0 +1,3 @@
+* Виправлення обміну файлами Jingle IBB
+* Повторювані виправлення правопису більше не заповнюють базу даних
+* Перехід на Last Message Correction v1.1

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

@@ -0,0 +1,4 @@
+* Користувачі можуть встановлювати своє прізвисько (нікнейм)
+* Відновлювати завантаження файлів, зашифрованих OMEMO
+* Канали тепер позначаються символом «#» на піктограмі
+* Quicksy за замовчуванням використовує «завжди» для шифрування OMEMO (приховує значок замка)

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

@@ -0,0 +1,2 @@
+* Виправлено вибір піктограми користувача на деяких пристроях з Android 10
+* Виправлення обміну файлами для великих файлів

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

@@ -0,0 +1 @@
+* Голосові та відеовиклики (необхідна підтримка сервера у вигляді серверів STUN і TURN, доступних для виявлення через XEP-0215)

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

@@ -0,0 +1,2 @@
+* Зворотний зв'язок (звуки «набір номера», «початок дзвінка», «завершення дзвінка») для голосових викликів
+* Виправлено проблему з повторною спробою невдалого відеовиклику

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

@@ -0,0 +1,2 @@
+* Додано кнопку перемикання камери під час відеовиклику
+* Виправлення для голосових дзвінків на планшетах

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

@@ -0,0 +1,3 @@
+* Значок дзвінка переміщено ліворуч, щоб інші значки панелі інструментів залишалися на відповідних місцях
+* Показувати тривалість розмови під час голосових викликів
+* Визначення переваги в голосових та відеовикликах (двоє людей телефонують один одному одночасно)

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

@@ -0,0 +1,2 @@
+* Перероблено інтерфейс входу з сертифікатом
+* Додано можливість закріплювати чати (додати до вибраного)

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

@@ -0,0 +1,3 @@
+* Зменшено відлуння під час викликів на деяких пристроях
+* Виправлено вхід з паролями, що містять спеціальні символи
+* Сигнали набору номера та зайнятості відтворюються через динамік під час відеовикликів

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

@@ -0,0 +1,3 @@
+* Показувати кнопку «Довідка» у випадку невдалого голосового чи відеовиклику
+* Виправлено деякі неприємні збої
+* Виправлено з'єднання Jingle (обмін файлами + дзвінки) з JID'ами без ресурсу

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

@@ -0,0 +1,2 @@
+* Виправлено сповіщення, які не з'являлися за певних умов
+* Виправлення проблем сумісності та збоїв, пов’язаних з голосовими та відеовикликами

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

@@ -0,0 +1,3 @@
+* Додано «Повернутися до чату» на екрані звукового виклику
+* Удосконалено комбінації клавіш
+* Виправлення помилок

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

@@ -0,0 +1,3 @@
+* Обробляти файли GPX
+* Покращення продуктивності при відновленні резервної копії
+* Виправлення помилок

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

@@ -0,0 +1,4 @@
+* Пошук в окремих розмовах
+* Сповіщення про невдале надсилання повідомлень
+* Імена (нікнейми) користувачів Quicksy зберігаються після перезапуску застосунку
+* Додано кнопку для запуску Orbot (Tor) із сповіщення, якщо це необхідно

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

@@ -0,0 +1,3 @@
+* Просте створення запрошень на серверах з підтримкою запрошень
+* Перегляд файлів GIF, отриманих з Movim
+* Піктограми користувачів зберігаються у кеші

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

@@ -0,0 +1,3 @@
+* Виправлено проблеми з підключенням, коли різні облікові записи використовували різні механізми SCRAM
+* Додано підтримку SCRAM-SHA-512
+* Дозволено обмін файлами P2P (Jingle) із власним контактом

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

@@ -0,0 +1,3 @@
+* Показувати кнопку виклику для контактів поза мережею, якщо вони раніше оголосили про підтримку дзвінків
+* Кнопка «Назад» більше не завершує дзвінок під час виклику
+* Виправлення помилок

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

@@ -0,0 +1,4 @@
+* Можливість вибирати мелодію для вхідних викликів
+* Виправлено виявлення ідентифікатора ключа OpenPGP для OpenKeychain 5.6+
+* Коректна перевірка сертифікатів punycode TLS
+* Покращення стабільності встановлення сесії RTP (дзвінки)

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

@@ -0,0 +1,2 @@
+* Перевіряти голосові та відеовиклики за допомогою вже існуючих сесій OMEMO
+* Покращено сумісність із реалізаціями WebRTC без libwebrtc

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

@@ -0,0 +1,2 @@
+* Завжди перевіряти ім'я домену. Без перезапису користувачем
+* Підтримка попередньої автентифікації списку контактів

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

@@ -0,0 +1,3 @@
+* Показувати чорні смуги, коли віддалене відео не відповідає пропорціям екрана
+* Покращення ефективності пошуку
+* Додано налаштування для заборони знімків екрана

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

@@ -0,0 +1,2 @@
+* Виправлено проблему коли деякі відео не стискалися
+* Виправлено рідкісний збій під час відкриття сповіщення

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

@@ -0,0 +1,11 @@
+Версія 2.10.9
+* Запитувати дозволи Bluetooth для голосових та відеовикликів (можна відхилити, якщо не використовуєте гарнітуру Bluetooth)
+* Виправлено помилку під час виклику Movim
+* Виправлено відображення неправильної піктограми для групових чатів
+* Завжди запитувати про вимкнення оптимізації батареї
+* Установлено прапорець «лише локально» для сповіщень «x облікових записів у мережі»
+* Виправлено взаємодію з плагіном Google Maps Share Location
+* Видалено примітку щодо плати за сервер
+* Зберігати файли в місці, яке підходить для Android 11
+* Пробувати повторно підключити виклик після перемикання мережі
+* Показувати JID абонента та JID облікового запису на екрані вхідного виклику

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

@@ -0,0 +1,5 @@
+* Реалізація Extensible SASL Profile, Bind 2.0 і Fast для швидшого повторного з'єднання
+* Реалізація Channel Binding
+* Додано можливість перемикатися з голосового на відеовиклик
+* Додано можливість видаляти свою піктограму користувача
+* Додано сповіщення про пропущені виклики

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

@@ -0,0 +1,2 @@
+* Виправлено циклічне повторне надсилання на сервери, які підтримують лише sm:2
+* Показувати «Перемкнути на відео» тільки якщо інша сторона підтримує відео

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

@@ -0,0 +1,3 @@
+* Виправлено повторне надсилання повідомлень при використанні SASL2
+* Виправлення чорного відео між деякими пристроями
+* Виправлено збій з порожніми паролями

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

@@ -0,0 +1 @@
+* Інтегрований дистриб'ютор UnifiedPush для надсилання push-повідомлень іншим застосункам, які підтримують UnifiedPush, як-от Tusky і Fedilab

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

@@ -0,0 +1,2 @@
+* Цільовий SDK знову підвищено до 33
+* Виправлення проблем із серверами, які підтримують SASL2 без вбудованого керування потоком

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

@@ -0,0 +1,2 @@
+* Підтримка налаштування сповіщень окремо для кожної розмови
+* Використання Opus для голосових повідомлень на Android 10

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

@@ -0,0 +1,3 @@
+* Підвищено залежність libwebrtc до M117 і оновлено libvpx
+* Повернення до AAC для голосових повідомлень
+* Підтримка своїх налаштувань мови в додатку

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

@@ -0,0 +1,3 @@
+* Підтримка приватної DNS (DNS over TLS)
+* Підтримка тематичного значка додатка в лаунчері
+* Виправлено рідкісну проблему з дозволами під час обміну файлами на Android 11 і новіших

fastlane/metadata/android/zh-CN/changelogs/349.txt 🔗

@@ -0,0 +1,4 @@
+* 引入专家设置在本地服务器上执行频道发现而不是 search.jabber.network
+* 默认启用传递复选标记并移除设置
+* 默认启用“发送按钮指示状态”并移除设置
+* 将备份和前台服务设置移至主屏幕

fastlane/metadata/android/zh-CN/changelogs/353.txt 🔗

@@ -0,0 +1,4 @@
+* 让用户设置自己的昵称
+* 恢复 OMEMO 加密文件的下载
+* 频道现在使用“#”作为头像中的符号
+* Quicksy 使用“始终”作为 OMEMO 加密默认值(隐藏锁定图标)

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

@@ -0,0 +1,11 @@
+版本2.10.9
+* 进行音视频通话时请求蓝牙权限(如果您不使用蓝牙耳机可以拒绝)
+* 修复呼叫 Movim 时的错误
+* 修复群组聊天的显示错误头像的问题
+* 始终要求选择退出电池优化
+* 在“x 个已连接账号”通知上设置仅本地标志
+* 修复与 Google 地图分享位置插件的交互
+* 移除有关服务器费用的脚注
+* 将文件存储在适合 Android 11 的位置
+* 网络切换后尝试重新连接通话
+* 在来电屏幕中显示来电者JID和帐户JID

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

@@ -0,0 +1,5 @@
+* 实施可扩展 SASL Profile、Bind 2.0 和 Fast,以加快重新连接速度
+* 实现频道绑定
+* 增加从音频通话切换到视频通话的功能
+* 增加删除自己头像的功能
+* 增加未接来电通知功能

gradle.properties 🔗

@@ -1,4 +1,6 @@
 android.useAndroidX=true
 android.enableJetifier=true
+android.nonTransitiveRClass=true
+android.nonFinalResIds=false
 org.gradle.jvmargs=-Xmx4096m
 org.gradle.daemon=false

gradle/wrapper/gradle-wrapper.properties 🔗

@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=97a52d145762adc241bad7fd18289bf7f6801e08ece6badf80402fe2b9f250b1
+distributionSha256Sum=5022b0b25fe182b0e50867e77f484501dba44feeea88f5c1f13b6b4660463640
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip

proguard-rules.pro 🔗

@@ -64,7 +64,21 @@
 -dontwarn retrofit2.KotlinExtensions
 -dontwarn retrofit2.KotlinExtensions$*
 
+
 # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
 # and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
 -if interface * { @retrofit2.http.* <methods>; }
 -keep,allowobfuscation interface <1>
+
+# Keep inherited services.
+-if interface * { @retrofit2.http.* <methods>; }
+-keep,allowobfuscation interface * extends <1>
+
+# With R8 full mode generic signatures are stripped for classes that are not
+# kept. Suspend functions are wrapped in continuations where the type argument
+# is used.
+-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
+
+# R8 full mode strips generic signatures from return types if not kept.
+-if interface * { @retrofit2.http.* public *** *(...); }
+-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

src/cheogram/res/values/themes.xml 🔗

@@ -105,6 +105,7 @@
         <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item>
         <item name="icon_cancel" type="reference">@drawable/ic_cancel_black_24dp</item>
         <item name="icon_copy" type="reference">@drawable/ic_content_copy_black_24dp</item>
+        <item name="icon_qr_code" type="reference">@drawable/ic_qr_code_black_24dp</item>
         <item name="icon_discard" type="reference">@drawable/ic_delete_white_24dp</item>
         <item name="icon_download" type="reference">@drawable/ic_file_download_white_24dp</item>
         <item name="icon_edit" type="reference">@drawable/ic_edit_white_24dp</item>
@@ -264,6 +265,7 @@
         <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item>
         <item name="icon_cancel" type="reference">@drawable/ic_cancel_white_24dp</item>
         <item name="icon_copy" type="reference">@drawable/ic_content_copy_white_24dp</item>
+        <item name="icon_qr_code" type="reference">@drawable/ic_qr_code_white_24dp</item>
         <item name="icon_discard" type="reference">@drawable/ic_delete_white_24dp</item>
         <item name="icon_download" type="reference">@drawable/ic_file_download_white_24dp</item>
         <item name="icon_edit" type="reference">@drawable/ic_edit_white_24dp</item>

src/conversations/fastlane/metadata/android/es-ES/full_description.txt 🔗

@@ -0,0 +1,39 @@
+Fácil de usar, fiable y con poca batería. Con soporte integrado para imágenes, chats de grupo y cifrado e2e.
+
+Principios de diseño:
+
+* Ser lo más bonito y fácil de usar posible sin sacrificar la seguridad ni la privacidad.
+* Basarse en protocolos existentes y bien establecidos.
+* No requerir una cuenta de Google o, específicamente, Google Cloud Messaging (GCM).
+* Requerir el menor número de permisos posible
+
+Características:
+
+* Cifrado de extremo a extremo con <a href="http://conversations.im/omemo/">OMEMO</a> o <a href="http://openpgp.org/about/">OpenPGP</a>.
+* Envío y recepción de imágenes
+* Llamadas de audio y vídeo cifradas (DTLS-SRTP)
+* Interfaz de usuario intuitiva que sigue las directrices de diseño de Android
+* Imágenes / Avatares para tus contactos
+* Sincronización con el cliente de escritorio
+* Conferencias (con soporte para marcadores)
+* Integración de la libreta de direcciones
+* Múltiples cuentas / bandeja de entrada unificada
+* Muy bajo impacto en la duración de la batería
+
+Conversations hace que sea muy fácil crear una cuenta en el servidor gratuito conversations.im. Sin embargo, Conversations también funciona con cualquier otro servidor XMPP. Muchos servidores XMPP están gestionados por voluntarios y son gratuitos.
+
+Características de XMPP:
+
+Conversations funciona con todos los servidores XMPP existentes. Sin embargo, XMPP es un protocolo extensible. Estas extensiones también están estandarizadas en los llamados XEP. Conversations soporta un par de ellas para mejorar la experiencia general del usuario. Existe la posibilidad de que su actual servidor XMPP no soporte estas extensiones. Por lo tanto, para sacar el máximo provecho de Conversaciones deberías considerar o bien cambiar a un servidor XMPP que lo haga o - mejor aún - ejecutar tu propio servidor XMPP para ti y tus amigos.
+
+Estos XEPs son (por el momento):
+
+* XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Se utilizará para transferir archivos si ambas partes están detrás de un cortafuegos (NAT).
+* XEP-0163: Protocolo de Evento Personal para avatares
+* XEP-0191: El comando de bloqueo te permite hacer una lista negra de spammers o bloquear contactos sin eliminarlos de tu lista.
+* XEP-0198: Stream Management permite a XMPP sobrevivir a pequeños cortes de red y cambios de la conexión TCP subyacente.
+* XEP-0280: Message Carbons que sincroniza automáticamente los mensajes que envías a tu cliente de escritorio y por lo tanto te permite cambiar sin problemas de tu cliente móvil a tu cliente de escritorio y viceversa en una sola conversación.
+* XEP-0237: Versionado de listas, principalmente para ahorrar ancho de banda en conexiones móviles deficientes.
+* XEP-0313: Gestión de Archivo de Mensajes sincroniza el historial de mensajes con el servidor. Ponerse al día con los mensajes que fueron enviados mientras Conversaciones estaba fuera de línea.
+* XEP-0352: Indicación del Estado del Cliente permite al servidor saber si Conversaciones está o no en segundo plano. Permite al servidor ahorrar ancho de banda reteniendo paquetes sin importancia.
+* XEP-0363: Carga de Archivos HTTP permite compartir archivos en conferencias y con contactos sin conexión. Requiere un componente adicional en su servidor.

src/conversations/fastlane/metadata/android/ro/full_description.txt 🔗

@@ -0,0 +1,38 @@
+Ușor de utilizat, fiabil, prietenos cu bateria. Cu suport încorporat pentru imagini, discuții de grup și criptare E2E.
+
+Principii de proiectare:
+
+* Să fie cât mai frumos și mai ușor de utilizat posibil, fără a sacrifica securitatea sau confidențialitatea.
+* Să se bazeze pe protocoale existente și bine stabilite
+* Nu necesită un cont Google sau în mod specific Google Cloud Messaging (GCM).
+* Să necesite cât mai puține permisiuni posibil
+
+Caracteristici:
+
+* Criptare de la un capăt-la-altul (E2E) cu <a href="http://conversations.im/omemo/">OMEMO</a> sau <a href="http://openpgp.org/about/">OpenPGP</a>
+* Trimiterea și primirea de imagini
+* Apeluri audio și video criptate (DTLS-SRTP)
+* Interfață intuitivă care respectă liniile directoare Android Design
+* Imagini / Avataruri pentru contactele dvs.
+* Se sincronizează cu clientul desktop
+* Conferințe (cu suport pentru marcaje)
+* Integrare cu lista de contacte
+* Conturi multiple / căsuță de mesaje unificată
+* Impact foarte redus asupra duratei de viață a bateriei
+
+Conversations face foarte ușoară crearea unui cont pe serverul gratuit conversations.im. Cu toate acestea, Conversations va funcționa și cu orice alt server XMPP. O mulțime de servere XMPP sunt administrate de voluntari și sunt gratuite.
+
+Caracteristici XMPP:
+
+Conversations funcționează cu orice server XMPP existent. Cu toate acestea, XMPP este un protocol extensibil. Aceste extensii sunt, de asemenea, standardizate în așa-numitele XEP-uri. Conversations suportă câteva dintre acestea pentru a îmbunătăți experiența generală a utilizatorului. Există o șansă ca serverul XMPP actual să nu suporte aceste extensii. Prin urmare, pentru a profita la maximum de Conversations, ar trebui să luați în considerare fie trecerea la un server XMPP care să suporte aceste extensii, fie - și mai bine - să rulați propriul server XMPP pentru dumneavoastră și prietenii dumneavoastră.
+
+Aceste XEP-uri sunt - deocamdată:
+* XEP-0065: SOCKS5 Bytestreams (sau mod_proxy65). Va fi utilizat pentru a transfera fișiere dacă ambele părți se află în spatele unui firewall (NAT).
+* XEP-0163: Protocol de evenimente personale pentru avatare.
+* XEP-0191: Comanda de blocare vă permite să puneți pe lista neagră spamerii sau să blocați contactele fără a le elimina din listă.
+* XEP-0198: Stream Management permite XMPP să supraviețuiască unor mici întreruperi de rețea și schimbărilor conexiunii TCP de bază.
+* XEP-0280: Message Carbons, care sincronizează automat mesajele pe care le trimiteți în clientul desktop și vă permite astfel să treceți fără probleme de la clientul mobil la clientul desktop și înapoi în cadrul unei singure conversații.
+* XEP-0237: Roster Versioning în principal pentru a economisi lățimea de bandă în cazul conexiunilor mobile slabe
+* XEP-0313: Gestionarea arhivei de mesaje sincronizează istoricul mesajelor cu serverul. Recuperați mesajele care au fost trimise în timp ce Conversations era deconectat.
+* XEP-0352: Client State Indication permite serverului să știe dacă Conversations este sau nu în fundal. Permite serverului să economisească lățimea de bandă prin reținerea pachetelor neimportante.
+* XEP-0363: HTTP File Upload vă permite să partajați fișiere în cadrul conferințelor și cu contactele deconectate. Necesită o componentă suplimentară pe serverul dumneavoastră.

src/conversations/fastlane/metadata/android/uk/full_description.txt 🔗

@@ -0,0 +1,39 @@
+Надійний, простий у використанні, ощадливо витрачає заряд акумулятора. Має вбудовану підтримку зображень, групових чатів і наскрізного шифрування.
+
+Принципи проєктування:
+
+* Бути максимально красивим та простим у використанні, не жертвуючи безпекою чи конфіденційністю
+* Покладатися на існуючі, добре встановлені протоколи
+* Не вимагати облікового запису Google, зокрема Google Cloud Messaging (GCM)
+* Вимагати якомога менше дозволів
+
+Функції:
+
+* Наскрізне шифрування (від відправника до одержувача) за допомогою <a href="http://conversations.im/omemo/">OMEMO</a> або <a href="http://openpgp.org/about/">OpenPGP</a>
+* Надсилання та отримання зображень
+* Зашифровані голосові та відеодзвінки (DTLS-SRTP)
+* Інтуїтивно зрозумілий інтерфейс користувача, який відповідає вказівкам Android Design
+* Зображення / Аватари для Ваших контактів
+* Синхронізація з настільним клієнтом
+* Конференції (з підтримкою закладок)
+* Інтеграція адресної книги
+* Кілька облікових записів / єдина папка вхідних
+* Дуже низький вплив на термін служби акумулятора
+
+Conversations дозволяє легко створити обліковий запис на безкоштовному сервері conversations.im. Однак Conversations працюватиме також із будь-яким іншим XMPP-сервером. Чимало серверів XMPP обслуговуються волонтерами і є безкоштовними.
+
+Функції XMPP:
+
+Conversations працює з будь-яким сервером XMPP. Проте XMPP — розширюваний протокол. Розширення також стандартизовані в так званих XEP. Conversations підтримує кілька з них, щоб покращити загальний досвід користування. Може виявитися, що Ваш поточний сервер XMPP не підтримує цих розширень. Тому, щоб отримати максимум від Conversations, розгляньте перехід на XMPP-сервер з підтримкою цих розширень або — ще краще — запускайте власний сервер XMPP для себе і своїх друзів.
+
+На даний час підтримуються такі XEP:
+
+* XEP-0065: SOCKS5 Bytestreams (або mod_proxy65). Використовується для передачі файлів, якщо обидві сторони знаходяться за брандмауером (NAT).
+* XEP-0163: персональний протокол подій для аватарів
+* XEP-0191: команда блокування дозволяє Вам заносити спамерів у чорний список або блокувати контакти, не видаляючи їх зі свого списку.
+* XEP-0198: керування потоками дозволяє XMPP витримувати невеликі перебої в мережі та зміни основного TCP-з'єднання.
+* XEP-0280: Message Carbons, який автоматично синхронізує повідомлення, які Ви надсилаєте, на настільний клієнт і, таким чином, дозволяє плавно переключатися з мобільного клієнта на клієнт для настільного ПК і назад протягом однієї розмови.
+* XEP-0237: версія списку в основному для економії пропускної здатності при поганих мобільних з'єднаннях
+* XEP-0313: керування архівом повідомлень синхронізує історію повідомлень із сервером. Дізнавайтеся про повідомлення, надіслані, поки Conversations був офлайн.
+* XEP-0352: індикація стану клієнта повідомляє серверу, чи працює Conversations у фоновому режимі. Дозволяє серверу заощаджувати пропускну здатність, утримуючи неважливі пакети.
+* XEP-0363: завантаження файлів HTTP дозволяє обмінюватися файлами в конференціях і з офлайн-контактами. Потрібен додатковий компонент на Вашому сервері.

src/conversations/fastlane/metadata/android/zh-CN/full_description.txt 🔗

@@ -0,0 +1,39 @@
+易于使用、性能可靠、电池友好。内置支持图片、群组聊天和 e2e 加密功能。
+
+设计原则:
+
+* 在不牺牲安全性和隐私性的前提下,尽可能美观易用
+* 依赖现有的、完善的协议
+* 不需要 Google 账号或特定的 Google 云通讯服务 (GCM)
+* 要求尽可能少的权限
+
+特点:
+
+* 使用 <a href="http://conversations.im/omemo/">OMEMO</a> 或 <a href="http://openpgp.org/about/">OpenPGP</a> 进行端对端加密
+* 发送和接收图片
+* 加密音视频通话 (DTLS-SRTP)
+* 直观的用户界面,遵循 Android 设计准则
+* 为您的联系人添加图片/头像
+* 与桌面客户端同步
+* 群聊(支持书签功能)
+* 通讯录集成
+* 多账号/统一收件箱
+* 对电池寿命的影响非常小
+
+Conversations 使在免费的 conversations.im 服务器上创建账号变得非常简单。不过,Conversations 也适用于任何其他 XMPP 服务器。许多 XMPP 服务器都是由志愿者免费运行的。
+
+XMPP 功能:
+
+Conversations 适用于所有 XMPP 服务器。然而,XMPP 是一种可扩展的协议。这些扩展在所谓的 XEP 中也是标准化的。Conversations 支持其中的一些扩展,以使整体用户体验更好。有一种可能是您当前的 XMPP 服务器不支持这些扩展。因此,要想充分使用 Conversations 的功能,您应该考虑切换到支持这些扩展的 XMPP 服务器,甚至有更好的方式,或者为您和您的朋友运行自己的 XMPP 服务器。
+
+到目前为止,这些 XEP 是:
+
+* XEP-0065:SOCKS5 字节流 (or mod_proxy65)。如果双方都在防火墙 (NAT) 后面,将用于传输文件。
+* XEP-0163:个人事件协议对于头像
+* XEP-0191:屏蔽指令可让您将垃圾邮件发送者列入黑名单或屏蔽的联系人中,而不会将其从花名册中删除。
+* XEP-0198:流管理允许 XMPP 在小规模网络中断和底层 TCP 连接发生变化时继续运行。
+* XEP-0280:消息抄送,可自动将您发送的消息同步到桌面客户端,因此您可以在一次对话中从手机客户端无缝切换到桌面客户端,然后再返回。
+* XEP-0237:花名册版本控制主要是为了在移动连接不佳的情况下节省带宽
+* XEP-0313:消息存档管理与服务器同步消息历史记录。补发 Conversations 离线时发送的消息。
+* XEP-0352:客户端状态指示让服务器知道 Conversations 是否在后台。允许服务器保留不重要的数据包,从而节省带宽。
+* XEP-0363:通过 HTTP 文件上传功能,您可以在群聊中与离线联系人分享文件。需要在服务器上安装额外组件。

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

@@ -6,6 +6,7 @@ import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
@@ -22,6 +23,21 @@ import androidx.core.app.NotificationManagerCompat;
 import com.google.common.base.Charsets;
 import com.google.common.base.Stopwatch;
 import com.google.common.io.CountingInputStream;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.xmpp.Jid;
 
 import org.bouncycastle.crypto.engines.AESEngine;
 import org.bouncycastle.crypto.io.CipherInputStream;
@@ -40,50 +56,47 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.WeakHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.ZipException;
 
 import javax.crypto.BadPaddingException;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.persistance.DatabaseBackend;
-import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.ui.ManageAccountActivity;
-import eu.siacs.conversations.utils.BackupFileHeader;
-import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
-import eu.siacs.conversations.xmpp.Jid;
-
 public class ImportBackupService extends Service {
 
     private static final int NOTIFICATION_ID = 21;
     private static final AtomicBoolean running = new AtomicBoolean(false);
     private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
-    private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
-    private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
+    private final SerialSingleThreadExecutor executor =
+            new SerialSingleThreadExecutor(getClass().getSimpleName());
+    private final Set<OnBackupProcessed> mOnBackupProcessedListeners =
+            Collections.newSetFromMap(new WeakHashMap<>());
     private DatabaseBackend mDatabaseBackend;
     private NotificationManager notificationManager;
 
-    private static int count(String input, char c) {
-        int count = 0;
-        for (char aChar : input.toCharArray()) {
-            if (aChar == c) {
-                ++count;
-            }
-        }
-        return count;
-    }
+    private static final Collection<String> TABLE_ALLOW_LIST =
+            Arrays.asList(
+                    Account.TABLENAME,
+                    Conversation.TABLENAME,
+                    Message.TABLENAME,
+                    SQLiteAxolotlStore.PREKEY_TABLENAME,
+                    SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+                    SQLiteAxolotlStore.SESSION_TABLENAME,
+                    SQLiteAxolotlStore.IDENTITIES_TABLENAME);
+    private static final Pattern COLUMN_PATTERN = Pattern.compile("^[a-zA-Z_]+$");
 
     @Override
     public void onCreate() {
         mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
-        notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+        notificationManager =
+                (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
     }
 
     @Override
@@ -105,16 +118,17 @@ public class ImportBackupService extends Service {
             return START_NOT_STICKY;
         }
         if (running.compareAndSet(false, true)) {
-            executor.execute(() -> {
-                startForegroundService();
-                final boolean success = importBackup(uri, password);
-                stopForeground(true);
-                running.set(false);
-                if (success) {
-                    notifySuccess();
-                }
-                stopSelf();
-            });
+            executor.execute(
+                    () -> {
+                        startForegroundService();
+                        final boolean success = importBackup(uri, password);
+                        stopForeground(true);
+                        running.set(false);
+                        if (success) {
+                            notifySuccess();
+                        }
+                        stopSelf();
+                    });
         } else {
             Log.d(Config.LOGTAG, "backup already running");
         }
@@ -126,42 +140,62 @@ public class ImportBackupService extends Service {
     }
 
     public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) {
-        executor.execute(() -> {
-            final List<Jid> accounts = mDatabaseBackend.getAccountJids(false);
-            final ArrayList<BackupFile> backupFiles = new ArrayList<>();
-            final Set<String> apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
-            final List<File> directories = new ArrayList<>();
-            for (final String app : apps) {
-                directories.add(FileBackend.getLegacyBackupDirectory(app));
-            }
-            directories.add(FileBackend.getBackupDirectory(this));
-            for (final File directory : directories) {
-                if (!directory.exists() || !directory.isDirectory()) {
-                    Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
-                    continue;
-                }
-                final File[] files = directory.listFiles();
-                if (files == null) {
-                    continue;
-                }
-                for (final File file : files) {
-                    if (file.isFile() && file.getName().endsWith(".ceb")) {
-                        try {
-                            final BackupFile backupFile = BackupFile.read(file);
-                            if (accounts.contains(backupFile.getHeader().getJid())) {
-                                Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid());
-                            } else {
-                                backupFiles.add(backupFile);
+        executor.execute(
+                () -> {
+                    final List<Jid> accounts = mDatabaseBackend.getAccountJids(false);
+                    final ArrayList<BackupFile> backupFiles = new ArrayList<>();
+                    final Set<String> apps =
+                            new HashSet<>(
+                                    Arrays.asList(
+                                            "Conversations",
+                                            "Quicksy",
+                                            getString(R.string.app_name)));
+                    final List<File> directories = new ArrayList<>();
+                    for (final String app : apps) {
+                        directories.add(FileBackend.getLegacyBackupDirectory(app));
+                    }
+                    directories.add(FileBackend.getBackupDirectory(this));
+                    for (final File directory : directories) {
+                        if (!directory.exists() || !directory.isDirectory()) {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    "directory not found: " + directory.getAbsolutePath());
+                            continue;
+                        }
+                        final File[] files = directory.listFiles();
+                        if (files == null) {
+                            continue;
+                        }
+                        Log.d(Config.LOGTAG, "looking for backups in " + directory);
+                        for (final File file : files) {
+                            if (file.isFile() && file.getName().endsWith(".ceb")) {
+                                try {
+                                    final BackupFile backupFile = BackupFile.read(file);
+                                    if (accounts.contains(backupFile.getHeader().getJid())) {
+                                        Log.d(
+                                                Config.LOGTAG,
+                                                "skipping backup for "
+                                                        + backupFile.getHeader().getJid());
+                                    } else {
+                                        backupFiles.add(backupFile);
+                                    }
+                                } catch (final IOException
+                                        | IllegalArgumentException
+                                        | BackupFileHeader.OutdatedBackupFileVersion e) {
+                                    Log.d(Config.LOGTAG, "unable to read backup file ", e);
+                                }
                             }
-                        } catch (IOException | IllegalArgumentException e) {
-                            Log.d(Config.LOGTAG, "unable to read backup file ", e);
                         }
                     }
-                }
-            }
-            Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString()));
-            onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
-        });
+                    Collections.sort(
+                            backupFiles,
+                            (a, b) ->
+                                    a.header
+                                            .getJid()
+                                            .toString()
+                                            .compareTo(b.header.getJid().toString()));
+                    onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
+                });
     }
 
     private void startForegroundService() {
@@ -180,14 +214,16 @@ public class ImportBackupService extends Service {
         }
         final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
         try {
-            notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress));
+            notificationManager.notify(
+                    NOTIFICATION_ID, createImportBackupNotification(max, progress));
         } catch (final RuntimeException e) {
             Log.d(Config.LOGTAG, "unable to make notification", e);
         }
     }
 
     private Notification createImportBackupNotification(final int max, final int progress) {
-        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+        NotificationCompat.Builder mBuilder =
+                new NotificationCompat.Builder(getBaseContext(), "backup");
         mBuilder.setContentTitle(getString(R.string.restoring_backup))
                 .setSmallIcon(R.drawable.ic_unarchive_white_24dp)
                 .setProgress(max, progress, max == 1 && progress == 0);
@@ -212,7 +248,9 @@ public class ImportBackupService extends Service {
                     fileSize = 0;
                 } else {
                     returnCursor.moveToFirst();
-                    fileSize = returnCursor.getLong(returnCursor.getColumnIndex(OpenableColumns.SIZE));
+                    fileSize =
+                            returnCursor.getLong(
+                                    returnCursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
                     returnCursor.close();
                 }
                 inputStream = getContentResolver().openInputStream(uri);
@@ -242,40 +280,46 @@ public class ImportBackupService extends Service {
             final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
 
             final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
-            cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv()));
-            final CipherInputStream cipherInputStream = new CipherInputStream(countingInputStream, cipher);
+            cipher.init(
+                    false,
+                    new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv()));
+            final CipherInputStream cipherInputStream =
+                    new CipherInputStream(countingInputStream, cipher);
 
             final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
-            final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8));
+            final BufferedReader reader =
+                    new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8));
+            final JsonReader jsonReader = new JsonReader(reader);
+            if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
+                jsonReader.beginArray();
+            } else {
+                throw new IllegalStateException("Backup file did not begin with array");
+            }
             db.beginTransaction();
-            String line;
-            StringBuilder multiLineQuery = null;
-            while ((line = reader.readLine()) != null) {
-                int count = count(line, '\'');
-                if (multiLineQuery != null) {
-                    multiLineQuery.append('\n');
-                    multiLineQuery.append(line);
-                    if (count % 2 == 1) {
-                        db.execSQL(multiLineQuery.toString());
-                        multiLineQuery = null;
-                        updateImportBackupNotification(fileSize, countingInputStream.getCount());
-                    }
-                } else {
-                    if (count % 2 == 0) {
-                        db.execSQL(line);
-                        updateImportBackupNotification(fileSize, countingInputStream.getCount());
-                    } else {
-                        multiLineQuery = new StringBuilder(line);
-                    }
+            while (jsonReader.hasNext()) {
+                if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
+                    importRow(db, jsonReader, backupFileHeader.getJid(), password);
+                } else if (jsonReader.peek() == JsonToken.END_ARRAY) {
+                    jsonReader.endArray();
+                    continue;
                 }
+                updateImportBackupNotification(fileSize, countingInputStream.getCount());
             }
             db.setTransactionSuccessful();
             db.endTransaction();
             final Jid jid = backupFileHeader.getJid();
-            final Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain().toEscapedString()});
+            final Cursor countCursor =
+                    db.rawQuery(
+                            "select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?",
+                            new String[] {
+                                jid.getEscapedLocal(), jid.getDomain().toEscapedString()
+                            });
             countCursor.moveToFirst();
             final int count = countCursor.getInt(0);
-            Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop().toString()));
+            Log.d(
+                    Config.LOGTAG,
+                    String.format(
+                            "restored %d messages in %s", count, stopwatch.stop().toString()));
             countCursor.close();
             stopBackgroundService();
             synchronized (mOnBackupProcessedListeners) {
@@ -286,7 +330,8 @@ public class ImportBackupService extends Service {
             return true;
         } catch (final Exception e) {
             final Throwable throwable = e.getCause();
-            final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException;
+            final boolean reasonWasCrypto =
+                    throwable instanceof BadPaddingException || e instanceof ZipException;
             synchronized (mOnBackupProcessedListeners) {
                 for (OnBackupProcessed l : mOnBackupProcessedListeners) {
                     if (reasonWasCrypto) {
@@ -301,14 +346,75 @@ public class ImportBackupService extends Service {
         }
     }
 
+    private void importRow(
+            final SQLiteDatabase db,
+            final JsonReader jsonReader,
+            final Jid account,
+            final String passphrase)
+            throws IOException {
+        jsonReader.beginObject();
+        final String firstParameter = jsonReader.nextName();
+        if (!firstParameter.equals("table")) {
+            throw new IllegalStateException("Expected key 'table'");
+        }
+        final String table = jsonReader.nextString();
+        if (!TABLE_ALLOW_LIST.contains(table)) {
+            throw new IOException(String.format("%s is not recognized for import", table));
+        }
+        final ContentValues contentValues = new ContentValues();
+        final String secondParameter = jsonReader.nextName();
+        if (!secondParameter.equals("values")) {
+            throw new IllegalStateException("Expected key 'values'");
+        }
+        jsonReader.beginObject();
+        while (jsonReader.peek() != JsonToken.END_OBJECT) {
+            final String name = jsonReader.nextName();
+            if (COLUMN_PATTERN.matcher(name).matches()) {
+                if (jsonReader.peek() == JsonToken.NULL) {
+                    jsonReader.nextNull();
+                    contentValues.putNull(name);
+                } else if (jsonReader.peek() == JsonToken.NUMBER) {
+                    contentValues.put(name, jsonReader.nextLong());
+                } else {
+                    contentValues.put(name, jsonReader.nextString());
+                }
+            } else {
+                throw new IOException(String.format("Unexpected column name %s", name));
+            }
+        }
+        jsonReader.endObject();
+        jsonReader.endObject();
+        if (Account.TABLENAME.equals(table)) {
+            final Jid jid =
+                    Jid.of(
+                            contentValues.getAsString(Account.USERNAME),
+                            contentValues.getAsString(Account.SERVER),
+                            null);
+            final String password = contentValues.getAsString(Account.PASSWORD);
+            if (jid.equals(account) && passphrase.equals(password)) {
+                Log.d(Config.LOGTAG, "jid and password from backup header had matching row");
+            } else {
+                throw new IOException("jid or password in table did not match backup");
+            }
+        }
+        db.insert(table, null, contentValues);
+    }
+
     private void notifySuccess() {
-        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+        NotificationCompat.Builder mBuilder =
+                new NotificationCompat.Builder(getBaseContext(), "backup");
         mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title))
                 .setContentText(getString(R.string.notification_restored_backup_subtitle))
                 .setAutoCancel(true)
-                .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), s()
-                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
-                        : PendingIntent.FLAG_UPDATE_CURRENT))
+                .setContentIntent(
+                        PendingIntent.getActivity(
+                                this,
+                                145,
+                                new Intent(this, ManageAccountActivity.class),
+                                s()
+                                        ? PendingIntent.FLAG_IMMUTABLE
+                                                | PendingIntent.FLAG_UPDATE_CURRENT
+                                        : PendingIntent.FLAG_UPDATE_CURRENT))
                 .setSmallIcon(R.drawable.ic_unarchive_white_24dp);
         notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
     }
@@ -391,4 +497,4 @@ public class ImportBackupService extends Service {
             return ImportBackupService.this;
         }
     }
-}
+}

src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java 🔗

@@ -30,6 +30,7 @@ import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
 import eu.siacs.conversations.services.ImportBackupService;
 import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
 import eu.siacs.conversations.ui.util.SettingsUtils;
+import eu.siacs.conversations.utils.BackupFileHeader;
 import eu.siacs.conversations.utils.ThemeHelper;
 
 public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
@@ -131,6 +132,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
         try {
             final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
             showEnterPasswordDialog(backupFile, finishOnCancel);
+        } catch (final BackupFileHeader.OutdatedBackupFileVersion e) {
+            Snackbar.make(binding.coordinator, R.string.outdated_backup_file_format, Snackbar.LENGTH_LONG).show();
         } catch (final IOException | IllegalArgumentException e) {
             Log.d(Config.LOGTAG, "unable to open backup file " + uri, e);
             Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();

src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java 🔗

@@ -359,6 +359,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 
     private void enableAccount(Account account) {
         account.setOption(Account.OPTION_DISABLED, false);
+        account.setOption(Account.OPTION_SOFT_DISABLED, false);
         final XmppConnection connection = account.getXmppConnection();
         if (connection != null) {
             connection.resetEverything();

src/conversations/res/drawable/ic_launcher_foreground.xml 🔗

@@ -1,24 +1,13 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:aapt="http://schemas.android.com/aapt"
-        android:width="108dp"
-        android:height="108dp"
-        android:viewportWidth="1146.7721"
-        android:viewportHeight="1146.7721">
-    <group android:translateX="322.69516"
-            android:translateY="317.38605">
-      <path
-          android:pathData="M253.219,17.719C126.144,17.719 22.469,118.884 22.469,243.75C22.469,368.616 126.138,469.844 253.219,469.844C292.739,469.844 323.216,461.736 358,449.094L468.469,493.625A14.556,14.562 0,0 0,488.063 476.625L458.125,355.656C477.356,321.886 483.938,283.416 483.938,243.75C483.938,118.887 380.293,17.719 253.219,17.719zM143.844,222C157.651,222 168.844,233.193 168.844,247C168.844,260.807 157.651,272 143.844,272C130.037,272 118.844,260.807 118.844,247C118.844,233.193 130.037,222 143.844,222zM253.563,222C267.37,222 278.563,233.193 278.563,247C278.563,260.807 267.37,272 253.563,272C239.755,272 228.563,260.807 228.563,247C228.563,233.193 239.755,222 253.563,222zM363.563,222C377.37,222 388.563,233.193 388.563,247C388.563,260.807 377.37,272 363.563,272C349.755,272 338.563,260.807 338.563,247C338.563,233.193 349.755,222 363.563,222z"
-          android:fillColor="#ffffff"
-          android:strokeColor="#00000000"
-          android:fillAlpha="1"/>
-      <path
-          android:pathData="M478.641,484.856 L447.361,358.245c19.89,-31.998 26.743,-69.572 26.743,-109.762 0,-116.817 -96.799,-211.484 -216.184,-211.484 -119.384,0 -216.184,94.667 -216.184,211.484 0,116.817 96.799,211.554 216.184,211.554 39.636,0 68.588,-8.142 105.194,-21.761z"
-          android:strokeAlpha="0"
-          android:strokeLineJoin="round"
-          android:strokeWidth="23.55835724"
-          android:fillColor="#00000000"
-          android:strokeColor="#000000"
-          android:fillAlpha="0"
-          android:strokeLineCap="butt"/>
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="1146.7721"
+    android:viewportHeight="1146.7721">
+    <group
+        android:translateX="322.69516"
+        android:translateY="317.38605">
+        <path
+            android:fillColor="#ffffff"
+            android:pathData="M253.219,17.719C126.144,17.719 22.469,118.884 22.469,243.75C22.469,368.616 126.138,469.844 253.219,469.844C292.739,469.844 323.216,461.736 358,449.094L468.469,493.625A14.556,14.562 0,0 0,488.063 476.625L458.125,355.656C477.356,321.886 483.938,283.416 483.938,243.75C483.938,118.887 380.293,17.719 253.219,17.719zM143.844,222C157.651,222 168.844,233.193 168.844,247C168.844,260.807 157.651,272 143.844,272C130.037,272 118.844,260.807 118.844,247C118.844,233.193 130.037,222 143.844,222zM253.563,222C267.37,222 278.563,233.193 278.563,247C278.563,260.807 267.37,272 253.563,272C239.755,272 228.563,260.807 228.563,247C228.563,233.193 239.755,222 253.563,222zM363.563,222C377.37,222 388.563,233.193 388.563,247C388.563,260.807 377.37,272 363.563,272C349.755,272 338.563,260.807 338.563,247C338.563,233.193 349.755,222 363.563,222z" />
     </group>
 </vector>

src/conversations/res/drawable/ic_launcher_monochrome.xml 🔗

@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="1146.7721"
+    android:viewportHeight="1146.7721">
+    <group
+        android:translateX="322.69516"
+        android:translateY="317.38605">
+        <path
+            android:fillColor="#000000"
+            android:pathData="M253.219,17.719C126.144,17.719 22.469,118.884 22.469,243.75C22.469,368.616 126.138,469.844 253.219,469.844C292.739,469.844 323.216,461.736 358,449.094L468.469,493.625A14.556,14.562 0,0 0,488.063 476.625L458.125,355.656C477.356,321.886 483.938,283.416 483.938,243.75C483.938,118.887 380.293,17.719 253.219,17.719zM143.844,222C157.651,222 168.844,233.193 168.844,247C168.844,260.807 157.651,272 143.844,272C130.037,272 118.844,260.807 118.844,247C118.844,233.193 130.037,222 143.844,222zM253.563,222C267.37,222 278.563,233.193 278.563,247C278.563,260.807 267.37,272 253.563,272C239.755,272 228.563,260.807 228.563,247C228.563,233.193 239.755,222 253.563,222zM363.563,222C377.37,222 388.563,233.193 388.563,247C388.563,260.807 377.37,272 363.563,272C349.755,272 338.563,260.807 338.563,247C338.563,233.193 349.755,222 363.563,222z" />
+    </group>
+</vector>

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

@@ -12,5 +12,5 @@
     <string name="tap_share_button_send_invite">Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s.</string>
     <string name="if_contact_is_nearby_use_qr">Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σαρώσει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας.</string>
     <string name="easy_invite_share_text">Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s</string>
-    <string name="share_invite_with">Διαμοιρασμός πρόσκλησης με...</string>
+    <string name="share_invite_with">Διαμοιρασμός πρόσκλησης με…</string>
 </resources>

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

@@ -10,5 +10,5 @@
     <string name="tap_share_button_send_invite">Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu.</string>
     <string name="if_contact_is_nearby_use_qr">Ak je váš kontakt blízko, na prijatie vašej pozvánky si môže nasnímať kód nižšie.</string>
     <string name="easy_invite_share_text">Pripojte sa k %1$sa rozprávajte sa so mnou: %2$s</string>
-    <string name="share_invite_with">Zdieľať pozvánku s...</string>
+    <string name="share_invite_with">Zdieľať pozvánku s…</string>
 </resources>

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

@@ -11,6 +11,6 @@
     <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="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 sohpet et: %2$s</string>
-    <string name="share_invite_with">Daveti şununla paylaş...</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>
 </resources>

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

@@ -3,10 +3,18 @@
     <string name="pick_a_server">Виберіть постачальника послуг обміну повідомленнями XMPP</string>
     <string name="use_conversations.im">Скористатися conversations.im</string>
     <string name="create_new_account">Створити новий обліковий запис</string>
-    <string name="do_you_have_an_account">Вже маєте обліковий запис XMPP? Можливо, користуєтеся іншою програмою XMPP або користувалися цією програмою раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз.\nЗверніть увагу, що деякі постачальники електронної пошти у той же час надають облікові записи XMPP.</string>
-    <string name="server_select_text">XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP сервером, який оберете.\nПроте, для зручності, ми спростили створення облікового запису на conversations.im — у постачальника, який спеціально налаштований на роботу з цією програмою.</string>
-    <string name="magic_create_text_on_x">Вас запросили до %1$s. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nОбираючи %1$s в якості свого постачальника, ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP.</string>
-    <string name="magic_create_text_fixed">Вас запросили до %1$s. Для вас створено ім\'я користувача. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомите їм свою повну адресу XMPP.</string>
+    <string name="do_you_have_an_account">Уже маєте обліковий запис XMPP\? Можливо, користуєтеся іншою програмою XMPP або користувалися Conversations раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз.
+\nЗверніть увагу, що деякі постачальники електронної пошти у той же час надають облікові записи XMPP.</string>
+    <string name="server_select_text">XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP-сервером, який оберете.
+\nПроте для зручності ми спростили створення облікового запису на conversations.im — у постачальника, спеціально налаштованого на роботу з Conversations.</string>
+    <string name="magic_create_text_on_x">Вас запросили до %1$s. Ми проведемо Вас крок за кроком, щоб створити обліковий запис.
+\nОбравши %1$s в якості свого постачальника, Ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP.</string>
+    <string name="magic_create_text_fixed">Вас запросили до %1$s. Для Вас створено ім\'я користувача. Ми проведемо Вас крок за кроком, щоб створити обліковий запис.
+\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP.</string>
     <string name="your_server_invitation">Ваше запрошення до сервера</string>
     <string name="improperly_formatted_provisioning">Неправильно відформатований код забезпечення</string>
-    </resources>
+    <string name="if_contact_is_nearby_use_qr">Якщо контакт поблизу, він також може прийняти запрошення, відсканувавши код нижче.</string>
+    <string name="easy_invite_share_text">Приєднуйтеся до %1$s і спілкуйтеся зі мною: %2$s</string>
+    <string name="share_invite_with">Запросити…</string>
+    <string name="tap_share_button_send_invite">Натисніть «Поділитися», щоб надіслати Вашому контакту запрошення до %1$s.</string>
+</resources>

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

@@ -2,15 +2,19 @@
 <resources>
     <string name="pick_a_server">选择您的 XMPP 提供者</string>
     <string name="use_conversations.im">使用 conversations.im</string>
-    <string name="create_new_account">创建新账户</string>
-    <string name="do_you_have_an_account">您有 XMPP 账户吗?如果您之前使用过其他的 XMPP 客户端,那么您已经拥有这种账户了。如果没有的话,您现在可以创建一个。\n提示:有些电子邮件服务也提供XMPP账户。</string>
-    <string name="server_select_text">XMPP 是独立于提供者的即时消息网络。您可以将此客户端与任意 XMPP 服务器一同使用。\n不过,您可以很容易地在 conversations.im 上创建账户;它是特别适合与“Conversations”一起使用的提供者。</string>
-    <string name="magic_create_text_on_x">您已受邀加入 %1$s。我们将指导您完成创建帐户的过程。\n使用 %1$s 作为提供者时,您可以通过您的完整 XMPP 地址与其他提供者的用户进行交流。</string>
-    <string name="magic_create_text_fixed">您已受邀加入 %1$s。已为您选择了一个用户名。我们将指导您完成创建帐户的过程。\n您可以使用完整的 XMPP 地址来与其他提供者的用户进行交流。</string>
-    <string name="your_server_invitation">你的服务器邀请</string>
-    <string name="improperly_formatted_provisioning">格式不正确的配置代码</string>
-    <string name="tap_share_button_send_invite">点击分享按钮向您的联系人发送加入 %1$s 的邀请。</string>
-    <string name="if_contact_is_nearby_use_qr">如果你的联系人在附近,他们也可以扫描下面的代码来接受你的邀请。</string>
+    <string name="create_new_account">创建新账号</string>
+    <string name="do_you_have_an_account">您已经有 XMPP 账号了吗?如果您之前使用过 Conversations 或其他 XMPP 客户端,那么您已经有这种账号了。如果没有,您可以立即创建一个。
+\n提示:一些电子邮件服务也提供 XMPP 账号。</string>
+    <string name="server_select_text">XMPP 是独立于提供者的即时通讯网络。您选择的任何 XMPP 服务器都可以使用此客户端。
+\n不过,您可以轻松地在 conversations.im 上创建账号;特别适合与 Conversations 使用的提供者。</string>
+    <string name="magic_create_text_on_x">您已受邀加入 %1$s。我们将指导您创建账号。
+\n当选择 %1$s 作为提供者时,向其他 XMPP 用户提供您的完整地址,就能和对方交流。</string>
+    <string name="magic_create_text_fixed">您已受邀加入 %1$s。已为您选择了用户名。我们将指导您创建账号。
+\n向其他 XMPP 用户提供您的完整地址,就能和对方交流。</string>
+    <string name="your_server_invitation">您的服务器邀请</string>
+    <string name="improperly_formatted_provisioning">配置代码格式不正确</string>
+    <string name="tap_share_button_send_invite">点击分享按钮,向您的联系人发送加入 %1$s 的邀请。</string>
+    <string name="if_contact_is_nearby_use_qr">如果您的联系人在附近,对方也可以扫描下方二维码接受邀请。</string>
     <string name="easy_invite_share_text">加入 %1$s 和我聊天:%2$s</string>
-    <string name="share_invite_with">分享邀请…</string>
+    <string name="share_invite_with">分享邀请至…</string>
 </resources>

src/main/AndroidManifest.xml 🔗

@@ -3,8 +3,13 @@
     xmlns:tools="http://schemas.android.com/tools">
 
     <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="32"
+        tools:ignore="ScopedStorage" />
+    <uses-permission
+        android:name="android.permission.READ_EXTERNAL_STORAGE"
+        android:maxSdkVersion="32" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.READ_PROFILE" />
     <uses-permission
@@ -19,7 +24,6 @@
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 
     <uses-feature
@@ -40,6 +44,19 @@
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
+    <!--    New permissions required to run as foreground service on Android 14.
+            SYSTEM_EXEMPTED is used when the app is on the doze allow list. This is normal
+            and the expected default behaviour. The other two hijack RECORD_AUDIO and CAMERA if they
+            happen to be granted. -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
+
+    <!-- this foreground service type permission is exclusively used for import and export backup -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
+
     <uses-feature
         android:name="android.hardware.camera"
         android:required="false" />
@@ -77,30 +94,60 @@
     <application
         android:allowBackup="true"
         android:appCategory="social"
+        android:dataExtractionRules="@xml/data_extraction_rules"
         android:fullBackupContent="@xml/backup_content"
         android:hardwareAccelerated="true"
         android:icon="@mipmap/new_launcher"
         android:label="@string/app_name"
         android:largeHeap="true"
+        android:localeConfig="@xml/locales_config"
         android:networkSecurityConfig="@xml/network_security_configuration"
         android:preserveLegacyExternalStorage="true"
         android:requestLegacyExternalStorage="true"
         android:theme="@style/ConversationsTheme"
-        tools:replace="android:label"
-        tools:targetApi="q">
+        tools:targetApi="tiramisu">
 
         <meta-data
             android:name="com.google.android.gms.car.application"
             android:resource="@xml/automotive_app_desc" />
 
-        <service android:name=".services.XmppConnectionService" android:foregroundServiceType="microphone" />
+        <!-- The warning that systemExempted requires alarm permission is incorrect because doze white list is sufficient -->
+        <service
+            android:name=".services.XmppConnectionService"
+            android:exported="false"
+            android:foregroundServiceType="specialUse|systemExempted|microphone|camera"
+            tools:ignore="ForegroundServicePermission">
+            <property
+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+                android:value="xmpp-im" />
+        </service>
+
+        <service
+            android:name=".services.ExportBackupService"
+            android:exported="false"
+            android:foregroundServiceType="dataSync" />
+
+        <service
+            android:name=".services.ImportBackupService"
+            android:exported="false"
+            android:foregroundServiceType="dataSync" />
+        <service
+            android:name=".services.ContactChooserTargetService"
+            android:exported="true"
+            android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.chooser.ChooserTargetService" />
+            </intent-filter>
+        </service>
 
         <receiver
             android:name=".services.EventReceiver"
-            android:exported="true">
+            android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+                <action
+                    android:name="android.net.conn.CONNECTIVITY_CHANGE"
+                    tools:ignore="BatteryLife" />
                 <action android:name="android.intent.action.ACTION_SHUTDOWN" />
                 <action android:name="android.media.RINGER_MODE_CHANGED" />
             </intent-filter>
@@ -147,7 +194,6 @@
         </activity>
         <activity
             android:name=".ui.ConversationsActivity"
-            android:label="@string/app_name"
             android:launchMode="singleTask"
             android:minWidth="300dp"
             android:minHeight="300dp"
@@ -159,8 +205,7 @@
             android:windowSoftInputMode="stateAlwaysHidden" />
         <activity
             android:name=".ui.UriHandlerActivity"
-            android:exported="true"
-            android:label="@string/app_name">
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 
@@ -281,7 +326,6 @@
         <activity
             android:name=".ui.ShareWithActivity"
             android:exported="true"
-            android:label="@string/app_name"
             android:launchMode="singleTop">
 
             <intent-filter>
@@ -327,19 +371,6 @@
             android:name=".ui.MediaBrowserActivity"
             android:label="@string/media_browser" />
 
-        <service android:name=".services.ExportBackupService" />
-        <service
-            android:name=".services.ImportBackupService"
-            android:exported="false" />
-        <service
-            android:name=".services.ContactChooserTargetService"
-            android:exported="true"
-            android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
-            <intent-filter>
-                <action android:name="android.service.chooser.ChooserTargetService" />
-            </intent-filter>
-        </service>
-
         <provider
             android:name="androidx.core.content.FileProvider"
             android:authorities="${applicationId}.files"

src/main/java/de/gultsch/minidns/AndroidDNSClient.java 🔗

@@ -0,0 +1,221 @@
+package de.gultsch.minidns;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.collection.LruCache;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+import de.measite.minidns.AbstractDNSClient;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.record.Data;
+
+import eu.siacs.conversations.Config;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+
+public class AndroidDNSClient extends AbstractDNSClient {
+
+    private static final long DNS_MAX_TTL = 86_400L;
+
+    private static final LruCache<QuestionServerTuple, DNSMessage> QUERY_CACHE =
+            new LruCache<>(1024);
+    private final Context context;
+    private final NetworkDataSource networkDataSource = new NetworkDataSource();
+    private boolean askForDnssec = false;
+
+    public AndroidDNSClient(final Context context) {
+        super();
+        this.setDataSource(networkDataSource);
+        this.context = context;
+    }
+
+    private static String getPrivateDnsServerName(final LinkProperties linkProperties) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            return linkProperties.getPrivateDnsServerName();
+        } else {
+            return null;
+        }
+    }
+
+    private static boolean isPrivateDnsActive(final LinkProperties linkProperties) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            return linkProperties.isPrivateDnsActive();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    protected DNSMessage.Builder newQuestion(final DNSMessage.Builder message) {
+        message.setRecursionDesired(true);
+        message.getEdnsBuilder()
+                .setUdpPayloadSize(networkDataSource.getUdpPayloadSize())
+                .setDnssecOk(askForDnssec);
+        return message;
+    }
+
+    @Override
+    protected DNSMessage query(final DNSMessage.Builder queryBuilder) throws IOException {
+        final DNSMessage question = newQuestion(queryBuilder).build();
+        for (final DNSServer dnsServer : getDNSServers()) {
+            final QuestionServerTuple cacheKey = new QuestionServerTuple(dnsServer, question);
+            final DNSMessage cachedResponse = queryCache(cacheKey);
+            if (cachedResponse != null) {
+                return cachedResponse;
+            }
+            final DNSMessage response = this.networkDataSource.query(question, dnsServer);
+            if (response == null) {
+                continue;
+            }
+            switch (response.responseCode) {
+                case NO_ERROR:
+                case NX_DOMAIN:
+                    break;
+                default:
+                    continue;
+            }
+            cacheQuery(cacheKey, response);
+            return response;
+        }
+        return null;
+    }
+
+    public boolean isAskForDnssec() {
+        return askForDnssec;
+    }
+
+    public void setAskForDnssec(boolean askForDnssec) {
+        this.askForDnssec = askForDnssec;
+    }
+
+    private List<DNSServer> getDNSServers() {
+        final ImmutableList.Builder<DNSServer> dnsServerBuilder = new ImmutableList.Builder<>();
+        final ConnectivityManager connectivityManager =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        final Network[] networks = getActiveNetworks(connectivityManager);
+        for (final Network network : networks) {
+            final LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
+            if (linkProperties == null) {
+                continue;
+            }
+            final String privateDnsServerName = getPrivateDnsServerName(linkProperties);
+            if (Strings.isNullOrEmpty(privateDnsServerName)) {
+                final boolean isPrivateDns = isPrivateDnsActive(linkProperties);
+                for (final InetAddress dnsServer : linkProperties.getDnsServers()) {
+                    if (isPrivateDns) {
+                        dnsServerBuilder.add(new DNSServer(dnsServer, Transport.TLS));
+                    } else {
+                        dnsServerBuilder.add(new DNSServer(dnsServer));
+                    }
+                }
+            } else {
+                dnsServerBuilder.add(new DNSServer(privateDnsServerName, Transport.TLS));
+            }
+        }
+        return dnsServerBuilder.build();
+    }
+
+    private Network[] getActiveNetworks(final ConnectivityManager connectivityManager) {
+        if (connectivityManager == null) {
+            return new Network[0];
+        }
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+            final Network activeNetwork = connectivityManager.getActiveNetwork();
+            if (activeNetwork != null) {
+                return new Network[] {activeNetwork};
+            }
+        }
+        return connectivityManager.getAllNetworks();
+    }
+
+    private DNSMessage queryCache(final QuestionServerTuple key) {
+        final DNSMessage cachedResponse;
+        synchronized (QUERY_CACHE) {
+            cachedResponse = QUERY_CACHE.get(key);
+            if (cachedResponse == null) {
+                return null;
+            }
+            final long expiresIn = expiresIn(cachedResponse);
+            if (expiresIn < 0) {
+                QUERY_CACHE.remove(key);
+                return null;
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                Log.d(
+                        Config.LOGTAG,
+                        "DNS query came from cache. expires in " + Duration.ofMillis(expiresIn));
+            }
+        }
+        return cachedResponse;
+    }
+
+    private void cacheQuery(final QuestionServerTuple key, final DNSMessage response) {
+        if (response.receiveTimestamp <= 0) {
+            return;
+        }
+        synchronized (QUERY_CACHE) {
+            QUERY_CACHE.put(key, response);
+        }
+    }
+
+    private static long ttl(final DNSMessage dnsMessage) {
+        final List<Record<? extends Data>> answerSection = dnsMessage.answerSection;
+        if (answerSection == null || answerSection.isEmpty()) {
+            final List<Record<? extends Data>> authoritySection = dnsMessage.authoritySection;
+            if (authoritySection == null || authoritySection.isEmpty()) {
+                return 0;
+            } else {
+                return Collections.min(Collections2.transform(authoritySection, d -> d.ttl));
+            }
+
+        } else {
+            return Collections.min(Collections2.transform(answerSection, d -> d.ttl));
+        }
+    }
+
+    private static long expiresAt(final DNSMessage dnsMessage) {
+        return dnsMessage.receiveTimestamp + (Math.min(DNS_MAX_TTL, ttl(dnsMessage)) * 1000L);
+    }
+
+    private static long expiresIn(final DNSMessage dnsMessage) {
+        return expiresAt(dnsMessage) - System.currentTimeMillis();
+    }
+
+    private static class QuestionServerTuple {
+        private final DNSServer dnsServer;
+        private final DNSMessage question;
+
+        private QuestionServerTuple(final DNSServer dnsServer, final DNSMessage question) {
+            this.dnsServer = dnsServer;
+            this.question = question.asNormalizedVersion();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            QuestionServerTuple that = (QuestionServerTuple) o;
+            return Objects.equal(dnsServer, that.dnsServer)
+                    && Objects.equal(question, that.question);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(dnsServer, question);
+        }
+    }
+}

src/main/java/de/gultsch/minidns/DNSServer.java 🔗

@@ -0,0 +1,104 @@
+package de.gultsch.minidns;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+public final class DNSServer {
+
+    public final InetAddress inetAddress;
+    public final String hostname;
+    public final int port;
+    public final List<Transport> transports;
+
+    public DNSServer(InetAddress inetAddress, Integer port, Transport transport) {
+        this.inetAddress = inetAddress;
+        this.port = port == null ? 0 : port;
+        this.transports = Collections.singletonList(transport);
+        this.hostname = null;
+    }
+
+    public DNSServer(final String hostname, final Integer port, final Transport transport) {
+        Preconditions.checkArgument(
+                Arrays.asList(Transport.HTTPS, Transport.TLS).contains(transport),
+                "hostname validation only works with TLS based transports");
+        this.hostname = hostname;
+        this.port = port == null ? 0 : port;
+        this.transports = Collections.singletonList(transport);
+        this.inetAddress = null;
+    }
+
+    public DNSServer(final String hostname, final Transport transport) {
+        this(hostname, Transport.DEFAULT_PORTS.get(transport), transport);
+    }
+
+    public DNSServer(InetAddress inetAddress, Transport transport) {
+        this(inetAddress, Transport.DEFAULT_PORTS.get(transport), transport);
+    }
+
+    public DNSServer(final InetAddress inetAddress) {
+        this(inetAddress, 53, Arrays.asList(Transport.UDP, Transport.TCP));
+    }
+
+    public DNSServer(final InetAddress inetAddress, int port, List<Transport> transports) {
+        this(inetAddress, null, port, transports);
+    }
+
+    private DNSServer(
+            final InetAddress inetAddress,
+            final String hostname,
+            final int port,
+            final List<Transport> transports) {
+        this.inetAddress = inetAddress;
+        this.hostname = hostname;
+        this.port = port;
+        this.transports = transports;
+    }
+
+    public Transport uniqueTransport() {
+        return Iterables.getOnlyElement(this.transports);
+    }
+
+    public DNSServer asUniqueTransport(final Transport transport) {
+        Preconditions.checkArgument(
+                this.transports.contains(transport),
+                "This DNS server does not have transport ",
+                transport);
+        return new DNSServer(inetAddress, hostname, port, Collections.singletonList(transport));
+    }
+
+    @Override
+    @Nonnull
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("inetAddress", inetAddress)
+                .add("hostname", hostname)
+                .add("port", port)
+                .add("transports", transports)
+                .toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DNSServer dnsServer = (DNSServer) o;
+        return port == dnsServer.port
+                && Objects.equal(inetAddress, dnsServer.inetAddress)
+                && Objects.equal(hostname, dnsServer.hostname)
+                && Objects.equal(transports, dnsServer.transports);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(inetAddress, hostname, port, transports);
+    }
+}

src/main/java/de/gultsch/minidns/DNSSocket.java 🔗

@@ -0,0 +1,199 @@
+package de.gultsch.minidns;
+
+import android.util.Log;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import de.measite.minidns.DNSMessage;
+
+import eu.siacs.conversations.Config;
+
+import org.conscrypt.OkHostnameVerifier;
+
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+final class DNSSocket implements Closeable {
+
+    public static final int QUERY_TIMEOUT = 5_000;
+
+    private final Semaphore semaphore = new Semaphore(1);
+    private final Map<Integer, SettableFuture<DNSMessage>> inFlightQueries = new HashMap<>();
+    private final Socket socket;
+    private final DataInputStream dataInputStream;
+    private final DataOutputStream dataOutputStream;
+
+    private DNSSocket(
+            final Socket socket,
+            final DataInputStream dataInputStream,
+            final DataOutputStream dataOutputStream) {
+        this.socket = socket;
+        this.dataInputStream = dataInputStream;
+        this.dataOutputStream = dataOutputStream;
+        new Thread(this::readDNSMessages).start();
+    }
+
+    private void readDNSMessages() {
+        try {
+            while (socket.isConnected()) {
+                final DNSMessage response = readDNSMessage();
+                final SettableFuture<DNSMessage> future;
+                synchronized (inFlightQueries) {
+                    future = inFlightQueries.remove(response.id);
+                }
+                if (future != null) {
+                    future.set(response);
+                } else {
+                    Log.e(Config.LOGTAG, "no in flight query found for response id " + response.id);
+                }
+            }
+            evictInFlightQueries(new EOFException());
+        } catch (final IOException e) {
+            evictInFlightQueries(e);
+        }
+    }
+
+    private void evictInFlightQueries(final Exception e) {
+        synchronized (inFlightQueries) {
+            final Iterator<Map.Entry<Integer, SettableFuture<DNSMessage>>> iterator =
+                    inFlightQueries.entrySet().iterator();
+            while (iterator.hasNext()) {
+                final Map.Entry<Integer, SettableFuture<DNSMessage>> entry = iterator.next();
+                entry.getValue().setException(e);
+                iterator.remove();
+            }
+        }
+    }
+
+    private static DNSSocket of(final Socket socket) throws IOException {
+        final DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
+        final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
+        return new DNSSocket(socket, dataInputStream, dataOutputStream);
+    }
+
+    public static DNSSocket connect(final DNSServer dnsServer) throws IOException {
+        switch (dnsServer.uniqueTransport()) {
+            case TCP:
+                return connectTcpSocket(dnsServer);
+            case TLS:
+                return connectTlsSocket(dnsServer);
+            default:
+                throw new IllegalStateException("This is not a socket based transport");
+        }
+    }
+
+    private static DNSSocket connectTcpSocket(final DNSServer dnsServer) throws IOException {
+        Preconditions.checkArgument(dnsServer.uniqueTransport() == Transport.TCP);
+        final SocketAddress socketAddress =
+                new InetSocketAddress(dnsServer.inetAddress, dnsServer.port);
+        final Socket socket = new Socket();
+        socket.connect(socketAddress, QUERY_TIMEOUT / 2);
+        socket.setSoTimeout(QUERY_TIMEOUT);
+        return DNSSocket.of(socket);
+    }
+
+    private static DNSSocket connectTlsSocket(final DNSServer dnsServer) throws IOException {
+        Preconditions.checkArgument(dnsServer.uniqueTransport() == Transport.TLS);
+        final SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+        final SSLSocket sslSocket = (SSLSocket) factory.createSocket();
+        if (Strings.isNullOrEmpty(dnsServer.hostname)) {
+            final SocketAddress socketAddress =
+                    new InetSocketAddress(dnsServer.inetAddress, dnsServer.port);
+            sslSocket.connect(socketAddress, QUERY_TIMEOUT / 2);
+            sslSocket.setSoTimeout(QUERY_TIMEOUT);
+            sslSocket.startHandshake();
+        } else {
+            final SocketAddress socketAddress = new InetSocketAddress(dnsServer.hostname, dnsServer.port);
+            sslSocket.connect(socketAddress, QUERY_TIMEOUT / 2);
+            sslSocket.setSoTimeout(QUERY_TIMEOUT);
+            sslSocket.startHandshake();
+            final SSLSession session = sslSocket.getSession();
+            final Certificate[] peerCertificates = session.getPeerCertificates();
+            if (peerCertificates.length == 0 || !(peerCertificates[0] instanceof X509Certificate)) {
+                throw new IOException("Peer did not provide X509 certificates");
+            }
+            final X509Certificate certificate = (X509Certificate) peerCertificates[0];
+            if (!OkHostnameVerifier.strictInstance().verify(dnsServer.hostname, certificate)) {
+                throw new SSLPeerUnverifiedException("Peer did not provide valid certificates");
+            }
+        }
+        return DNSSocket.of(sslSocket);
+    }
+
+    public DNSMessage query(final DNSMessage query) throws IOException, InterruptedException {
+        try {
+            return queryAsync(query).get(QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
+        } catch (final ExecutionException e) {
+            final Throwable cause = e.getCause();
+            if (cause instanceof IOException) {
+                throw (IOException) cause;
+            } else {
+                throw new IOException(e);
+            }
+        } catch (final TimeoutException e) {
+            throw new IOException(e);
+        }
+    }
+
+    public ListenableFuture<DNSMessage> queryAsync(final DNSMessage query)
+            throws InterruptedException, IOException {
+        final SettableFuture<DNSMessage> responseFuture = SettableFuture.create();
+        synchronized (this.inFlightQueries) {
+            this.inFlightQueries.put(query.id, responseFuture);
+        }
+        this.semaphore.acquire();
+        try {
+            query.writeTo(this.dataOutputStream);
+            this.dataOutputStream.flush();
+        } finally {
+            this.semaphore.release();
+        }
+        return responseFuture;
+    }
+
+    private DNSMessage readDNSMessage() throws IOException {
+        final int length = this.dataInputStream.readUnsignedShort();
+        byte[] data = new byte[length];
+        int read = 0;
+        while (read < length) {
+            read += this.dataInputStream.read(data, read, length - read);
+        }
+        return new DNSMessage(data);
+    }
+
+    @Override
+    public void close() throws IOException {
+        this.socket.close();
+    }
+
+    public void closeQuietly() {
+        try {
+            this.socket.close();
+        } catch (final IOException ignored) {
+
+        }
+    }
+}

src/main/java/de/gultsch/minidns/NetworkDataSource.java 🔗

@@ -0,0 +1,160 @@
+package de.gultsch.minidns;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.RemovalListener;
+import com.google.common.collect.ImmutableList;
+
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.MiniDNSException;
+import de.measite.minidns.source.DNSDataSource;
+import de.measite.minidns.util.MultipleIoException;
+
+import eu.siacs.conversations.Config;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+public class NetworkDataSource extends DNSDataSource {
+
+    private static final LoadingCache<DNSServer, DNSSocket> socketCache =
+            CacheBuilder.newBuilder()
+                    .removalListener(
+                            (RemovalListener<DNSServer, DNSSocket>)
+                                    notification -> {
+                                        final DNSServer dnsServer = notification.getKey();
+                                        final DNSSocket dnsSocket = notification.getValue();
+                                        if (dnsSocket == null) {
+                                            return;
+                                        }
+                                        Log.d(Config.LOGTAG, "closing connection to " + dnsServer);
+                                        dnsSocket.closeQuietly();
+                                    })
+                    .expireAfterAccess(5, TimeUnit.MINUTES)
+                    .build(
+                            new CacheLoader<DNSServer, DNSSocket>() {
+                                @Override
+                                @NonNull
+                                public DNSSocket load(@NonNull final DNSServer dnsServer)
+                                        throws Exception {
+                                    Log.d(Config.LOGTAG, "establishing connection to " + dnsServer);
+                                    return DNSSocket.connect(dnsServer);
+                                }
+                            });
+
+    private static List<Transport> transportsForPort(final int port) {
+        final ImmutableList.Builder<Transport> transportBuilder = new ImmutableList.Builder<>();
+        for (final Map.Entry<Transport, Integer> entry : Transport.DEFAULT_PORTS.entrySet()) {
+            if (entry.getValue().equals(port)) {
+                transportBuilder.add(entry.getKey());
+            }
+        }
+        return transportBuilder.build();
+    }
+
+    @Override
+    public DNSMessage query(final DNSMessage message, final InetAddress address, final int port)
+            throws IOException {
+        final List<Transport> transports = transportsForPort(port);
+        Log.w(
+                Config.LOGTAG,
+                "using legacy DataSource interface. guessing transports "
+                        + transports
+                        + " from port");
+        if (transports.isEmpty()) {
+            throw new IOException(String.format("No transports found for port %d", port));
+        }
+        return query(message, new DNSServer(address, port, transports));
+    }
+
+    public DNSMessage query(final DNSMessage message, final DNSServer dnsServer)
+            throws IOException {
+        Log.d(Config.LOGTAG, "using " + dnsServer);
+        final List<IOException> ioExceptions = new ArrayList<>();
+        for (final Transport transport : dnsServer.transports) {
+            try {
+                final DNSMessage response =
+                        queryWithUniqueTransport(message, dnsServer.asUniqueTransport(transport));
+                if (response != null && !response.truncated) {
+                    return response;
+                }
+            } catch (final IOException e) {
+                ioExceptions.add(e);
+            } catch (final InterruptedException e) {
+                throw new IOException(e);
+            }
+        }
+        MultipleIoException.throwIfRequired(ioExceptions);
+        return null;
+    }
+
+    private DNSMessage queryWithUniqueTransport(final DNSMessage message, final DNSServer dnsServer)
+            throws IOException, InterruptedException {
+        final Transport transport = dnsServer.uniqueTransport();
+        switch (transport) {
+            case UDP:
+                return queryUdp(message, dnsServer.inetAddress, dnsServer.port);
+            case TCP:
+            case TLS:
+                return queryDnsSocket(message, dnsServer);
+            default:
+                throw new IOException(
+                        String.format("Transport %s has not been implemented", transport));
+        }
+    }
+
+    protected DNSMessage queryUdp(
+            final DNSMessage message, final InetAddress address, final int port)
+            throws IOException {
+        final DatagramPacket request = message.asDatagram(address, port);
+        final byte[] buffer = new byte[udpPayloadSize];
+        try (final DatagramSocket socket = new DatagramSocket()) {
+            socket.setSoTimeout(timeout);
+            socket.send(request);
+            final DatagramPacket response = new DatagramPacket(buffer, buffer.length);
+            socket.receive(response);
+            DNSMessage dnsMessage = new DNSMessage(response.getData());
+            if (dnsMessage.id != message.id) {
+                throw new MiniDNSException.IdMismatch(message, dnsMessage);
+            }
+            return dnsMessage;
+        }
+    }
+
+    protected DNSMessage queryDnsSocket(final DNSMessage message, final DNSServer dnsServer)
+            throws IOException, InterruptedException {
+        final DNSSocket cachedDnsSocket = socketCache.getIfPresent(dnsServer);
+        if (cachedDnsSocket != null) {
+            try {
+                return cachedDnsSocket.query(message);
+            } catch (final IOException e) {
+                Log.d(
+                        Config.LOGTAG,
+                        "IOException occurred at cached socket. invalidating and falling through to new socket creation");
+                socketCache.invalidate(dnsServer);
+            }
+        }
+        try {
+            return socketCache.get(dnsServer).query(message);
+        } catch (final ExecutionException e) {
+            final Throwable cause = e.getCause();
+            if (cause instanceof IOException) {
+                throw (IOException) cause;
+            } else {
+                throw new IOException(cause);
+            }
+        }
+    }
+}

src/main/java/de/gultsch/minidns/Transport.java 🔗

@@ -0,0 +1,23 @@
+package de.gultsch.minidns;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+public enum Transport {
+    UDP,
+    TCP,
+    TLS,
+    HTTPS;
+
+    public static final Map<Transport, Integer> DEFAULT_PORTS;
+
+    static {
+        final ImmutableMap.Builder<Transport, Integer> builder = new ImmutableMap.Builder<>();
+        builder.put(Transport.UDP, 53);
+        builder.put(Transport.TCP, 53);
+        builder.put(Transport.TLS, 853);
+        builder.put(Transport.HTTPS, 443);
+        DEFAULT_PORTS = builder.build();
+    }
+}

src/main/java/eu/siacs/conversations/Config.java 🔗

@@ -84,6 +84,8 @@ public final class Config {
 
     public static final boolean XEP_0392 = true; //enables XEP-0392 v0.6.0
 
+
+    // media file formats. Homogenous Android or Conversations only deployments can switch to opus and webp
     public static final int AVATAR_SIZE = 192;
     public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG;
     public static final int AVATAR_CHAR_LIMIT = 9400;
@@ -92,6 +94,8 @@ public final class Config {
     public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG;
     public static final int IMAGE_QUALITY = 75;
 
+    public static final boolean USE_OPUS_VOICE_MESSAGES = true;
+
     public static final int MESSAGE_MERGE_WINDOW = 20;
 
     public static final int PAGE_SIZE = 50;

src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java 🔗

@@ -156,7 +156,8 @@ public class PgpDecryptionService {
 									&& manager.getAutoAcceptFileSize() > 0) {
 								manager.createNewDownloadConnection(message);
 							}
-						} catch (IOException e) {
+						} catch (final IOException e) {
+							Log.d(Config.LOGTAG,"decryption failed", e);
 							message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
 						}
 						mXmppConnectionService.updateMessage(message);
@@ -170,6 +171,7 @@ public class PgpDecryptionService {
 						}
 						break;
 					case OpenPgpApi.RESULT_CODE_ERROR:
+						Log.d(Config.LOGTAG,"decryption failed (api error)");
 						message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
 						mXmppConnectionService.updateMessage(message);
 						break;

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -736,8 +736,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         return axolotlStore.getFingerprintCertificate(fingerprint);
     }
 
-    public void setFingerprintTrust(String fingerprint, FingerprintStatus status) {
+    public void setFingerprintTrust(final String fingerprint, final FingerprintStatus status) {
         axolotlStore.setFingerprintStatus(fingerprint, status);
+        // TODO we decided to call this after a fingerprint gets toggled to update the 'your contact
+        //  is using unverified devices text'; however this means the entire screen gets redrawn
+        //  after a toggle which might be annoying or cause other weird UI glitches
+        mXmppConnectionService.updateAccountUi();
     }
 
     private ListenableFuture<XmppAxolotlSession> verifySessionWithPEP(final XmppAxolotlSession session) {

src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java 🔗

@@ -97,6 +97,10 @@ public class FingerprintStatus implements Comparable<FingerprintStatus> {
         return trust == Trust.TRUSTED || isVerified();
     }
 
+    public boolean isUnverified() {
+        return trust == Trust.TRUSTED;
+    }
+
     public boolean isVerified() {
         return trust == Trust.VERIFIED || trust == Trust.VERIFIED_X509;
     }

src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java 🔗

@@ -117,4 +117,14 @@ public enum ChannelBinding {
                 throw new AssertionError("Missing short name for " + channelBinding);
         }
     }
+
+    public static int priority(final ChannelBinding channelBinding) {
+        if (Arrays.asList(TLS_EXPORTER,TLS_UNIQUE).contains(channelBinding)) {
+            return 2;
+        } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
 }

src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java 🔗

@@ -97,4 +97,13 @@ public interface ChannelBindingMechanism {
         messageDigest.update(encodedCertificate);
         return messageDigest.digest();
     }
+
+    static int getPriority(final SaslMechanism mechanism) {
+        if (mechanism instanceof ChannelBindingMechanism) {
+            final ChannelBindingMechanism channelBindingMechanism = (ChannelBindingMechanism) mechanism;
+            return ChannelBinding.priority(channelBindingMechanism.getChannelBinding());
+        } else {
+            return 0;
+        }
+    }
 }

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -34,7 +34,6 @@ import eu.siacs.conversations.crypto.sasl.HashedToken;
 import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
 import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
 import eu.siacs.conversations.crypto.sasl.SaslMechanism;
-import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.UIHelper;
@@ -74,6 +73,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     public static final int OPTION_UNVERIFIED = 8;
     public static final int OPTION_FIXED_USERNAME = 9;
     public static final int OPTION_QUICKSTART_AVAILABLE = 10;
+    public static final int OPTION_SOFT_DISABLED = 11;
 
     private static final String KEY_PGP_SIGNATURE = "pgp_signature";
     private static final String KEY_PGP_ID = "pgp_id";
@@ -271,11 +271,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         return !isOptionSet(Account.OPTION_DISABLED);
     }
 
+    public boolean isConnectionEnabled() {
+        return !isOptionSet(Account.OPTION_DISABLED) && !isOptionSet(Account.OPTION_SOFT_DISABLED);
+    }
+
     public boolean isOptionSet(final int option) {
         return ((options & (1 << option)) != 0);
     }
 
     public boolean setOption(final int option, final boolean value) {
+        if (value && (option == OPTION_DISABLED || option == OPTION_SOFT_DISABLED)) {
+            this.setStatus(State.OFFLINE);
+        }
         final int before = this.options;
         if (value) {
             this.options |= 1 << option;
@@ -345,11 +352,17 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     public State getStatus() {
         if (isOptionSet(OPTION_DISABLED)) {
             return State.DISABLED;
+        } else if (isOptionSet(OPTION_SOFT_DISABLED)) {
+            return State.LOGGED_OUT;
         } else {
             return this.status;
         }
     }
 
+    public boolean unauthorized() {
+        return this.status == State.UNAUTHORIZED || this.lastErrorStatus == State.UNAUTHORIZED;
+    }
+
     public State getLastErrorStatus() {
         return this.lastErrorStatus;
     }
@@ -787,6 +800,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
 
     public enum State {
         DISABLED(false, false),
+        LOGGED_OUT(false,false),
         OFFLINE(false),
         CONNECTING(false),
         ONLINE(false),
@@ -812,6 +826,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         BIND_FAILURE,
         HOST_UNKNOWN,
         STREAM_ERROR,
+        SEE_OTHER_HOST,
         STREAM_OPENING_ERROR,
         POLICY_VIOLATION,
         PAYMENT_REQUIRED,
@@ -845,6 +860,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
             switch (this) {
                 case DISABLED:
                     return R.string.account_status_disabled;
+                case LOGGED_OUT:
+                    return R.string.account_state_logged_out;
                 case ONLINE:
                     return R.string.account_status_online;
                 case CONNECTING:
@@ -899,6 +916,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
                     return R.string.account_status_stream_opening_error;
                 case PAYMENT_REQUIRED:
                     return R.string.payment_required;
+                case SEE_OTHER_HOST:
+                    return R.string.reconnect_on_other_host;
                 case MISSING_INTERNET_PERMISSION:
                     return R.string.missing_internet_permission;
                 case TEMPORARY_AUTH_FAILURE:

src/main/java/eu/siacs/conversations/entities/MucOptions.java 🔗

@@ -176,8 +176,9 @@ public class MucOptions {
     }
 
     public boolean participantsCanChangeSubject() {
-        Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_changesubject");
-        if (field == null) field = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
+        final Field configField = getRoomInfoForm().getFieldByName("muc#roomconfig_changesubject");
+        final Field infoField = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
+        final Field field = configField != null ? configField : infoField;
         return field != null && "1".equals(field.getValue());
     }
 

src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java 🔗

@@ -17,6 +17,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import eu.siacs.conversations.xml.Element;
@@ -100,9 +101,9 @@ public class ServiceDiscoveryResult {
 
 	public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
 		this(
-				cursor.getString(cursor.getColumnIndex(HASH)),
-				Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT),
-				new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT)))
+				cursor.getString(cursor.getColumnIndexOrThrow(HASH)),
+				Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT),
+				new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT)))
 		);
 	}
 
@@ -213,24 +214,23 @@ public class ServiceDiscoveryResult {
 					.append("<");
 		}
 
-		List<String> features = this.getFeatures();
+		final List<String> features = this.getFeatures();
 		Collections.sort(features);
-
-		for (String feature : features) {
+		for (final String feature : features) {
 			s.append(clean(feature)).append("<");
 		}
 
-		Collections.sort(forms, (lhs, rhs) -> lhs.getFormType().compareTo(rhs.getFormType()));
-
-		for (Data form : forms) {
+		Collections.sort(forms, Comparator.comparing(Data::getFormType));
+		for (final Data form : forms) {
 			s.append(clean(form.getFormType())).append("<");
-			List<Field> fields = form.getFields();
-			Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName())));
-			for (Field field : fields) {
+			final List<Field> fields = form.getFields();
+			Collections.sort(
+                    fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName())));
+			for (final Field field : fields) {
 				s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
-				List<String> values = field.getValues();
-				Collections.sort(values);
-				for (String value : values) {
+				final List<String> values = field.getValues();
+				Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull));
+				for (final String value : values) {
 					s.append(blankNull(value)).append("<");
 				}
 			}

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -224,8 +224,8 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket invite(Conversation conversation, Jid contact) {
-        MessagePacket packet = new MessagePacket();
+    public MessagePacket invite(final Conversation conversation, final Jid contact) {
+        final MessagePacket packet = new MessagePacket();
         packet.setTo(conversation.getJid().asBareJid());
         packet.setFrom(conversation.getAccount().getJid());
         Element x = new Element("x");

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -63,7 +63,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 
     private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
 
-    private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract");
+    private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
+            Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing");
 
     public MessageParser(XmppConnectionService service) {
         super(service);
@@ -180,14 +181,19 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         return null;
     }
 
-    private Invite extractInvite(Element message) {
+    private Invite extractInvite(final Element message) {
         final Element mucUser = message.findChild("x", Namespace.MUC_USER);
         if (mucUser != null) {
-            Element invite = mucUser.findChild("invite");
+            final Element invite = mucUser.findChild("invite");
             if (invite != null) {
-                String password = mucUser.findChildContent("password");
-                Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
-                Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
+                final String password = mucUser.findChildContent("password");
+                final Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
+                final Jid to = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("to"));
+                if (to != null && from == null) {
+                    Log.d(Config.LOGTAG,"do not parse outgoing mediated invite "+message);
+                    return null;
+                }
+                final Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
                 if (room == null) {
                     return null;
                 }
@@ -494,8 +500,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 
         final Invite invite = extractInvite(packet);
         if (invite != null) {
-            if (isTypeGroupChat) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because type=groupchat");
+            if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
+                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite to "+invite.jid+" because it matches account");
+            } else if (isTypeGroupChat) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because it was received as group chat");
             } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) {
                 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC");
             } else {
@@ -1016,9 +1024,22 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                             if (serverMsgId == null) {
                                 serverMsgId = extractStanzaId(account, packet);
                             }
-                            mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, remoteMsgId, serverMsgId, timestamp);
-                            if (!account.getJid().asBareJid().equals(from.asBareJid()) && remoteMsgId != null) {
-                                processMessageReceipts(account, packet, remoteMsgId, query);
+                            mXmppConnectionService
+                                    .getJingleConnectionManager()
+                                    .deliverMessage(
+                                            account,
+                                            packet.getTo(),
+                                            packet.getFrom(),
+                                            child,
+                                            remoteMsgId,
+                                            serverMsgId,
+                                            timestamp);
+                            final Contact contact = account.getRoster().getContact(from);
+                            if (mXmppConnectionService.confirmMessages()
+                                    && !contact.isSelf()
+                                    && remoteMsgId != null
+                                    && contact.showInContactList()) {
+                                processMessageReceipts(account, packet, remoteMsgId, null);
                             }
                         } else if (query.isCatchup()) {
                             if ("propose".equals(action)) {

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -684,8 +684,13 @@ public class FileBackend {
         }
     }
 
-    public String getOriginalPath(Uri uri) {
-        return FileUtils.getPath(mXmppConnectionService, uri);
+    public String getOriginalPath(final Uri uri) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            // On Android 11+ we don’t have access to the original file
+            return null;
+        } else {
+            return FileUtils.getPath(mXmppConnectionService, uri);
+        }
     }
 
     public void copyFileToDocumentFile(Context ctx, File file, DocumentFile df) throws FileCopyException {

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

@@ -1,6 +1,7 @@
 package eu.siacs.conversations.services;
 
 import android.content.Intent;
+import android.os.Build;
 
 import eu.siacs.conversations.BuildConfig;
 
@@ -25,6 +26,10 @@ public abstract class AbstractQuickConversationsService {
         return "conversations".equals(BuildConfig.FLAVOR_mode);
     }
 
+    public static boolean isPlayStoreFlavor() {
+        return "playstore".equals(BuildConfig.FLAVOR_distribution);
+    }
+
     public abstract void signalAccountStateChange();
 
     public abstract boolean isSynchronizing();

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

@@ -128,7 +128,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
 	public Bitmap getRoundedShortcut(final MucOptions mucOptions) {
 		final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
 		final int size = Math.round(metrics.density * 48);
-		Bitmap bitmap = FileBackend.drawDrawable(get(mucOptions, size, false));
+		final Bitmap bitmap = FileBackend.drawDrawable(get(mucOptions, size, false));
 		final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
 		final Canvas canvas = new Canvas(output);
 		final Paint paint = new Paint();

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

@@ -25,6 +25,7 @@ import android.os.Bundle;
 import android.os.SystemClock;
 import android.os.Vibrator;
 import android.preference.PreferenceManager;
+import android.provider.Settings;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.text.SpannableString;
@@ -47,6 +48,7 @@ import androidx.core.graphics.drawable.IconCompat;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 
 import java.io.File;
@@ -720,6 +722,14 @@ public class NotificationService {
         builder.setCategory(NotificationCompat.CATEGORY_CALL);
         builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101));
         builder.setOngoing(true);
+        builder.addAction(
+                new NotificationCompat.Action.Builder(
+                                R.drawable.ic_call_end_white_48dp,
+                                mXmppConnectionService.getString(R.string.hang_up),
+                                createCallAction(
+                                        id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
+                        .build());
+        builder.setLocalOnly(true);
         return builder.build();
     }
 
@@ -850,6 +860,25 @@ public class NotificationService {
         }
     }
 
+    public void clearMissedCall(final Message message) {
+        synchronized (mMissedCalls) {
+            final Iterator<Map.Entry<Conversational,MissedCallsInfo>> iterator = mMissedCalls.entrySet().iterator();
+            while (iterator.hasNext()) {
+                final Map.Entry<Conversational, MissedCallsInfo> entry = iterator.next();
+                final Conversational conversational = entry.getKey();
+                final MissedCallsInfo missedCallsInfo = entry.getValue();
+                if (conversational.getUuid().equals(message.getConversation().getUuid())) {
+                    if (missedCallsInfo.removeMissedCall()) {
+                        cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID);
+                        Log.d(Config.LOGTAG,conversational.getAccount().getJid().asBareJid()+": dismissed missed call because call was picked up on other device");
+                        iterator.remove();
+                    }
+                }
+            }
+            updateMissedCallNotifications(null);
+        }
+    }
+
     public void clearMissedCalls() {
         synchronized (mMissedCalls) {
             for (final Conversational conversation : mMissedCalls.keySet()) {
@@ -1375,8 +1404,8 @@ public class NotificationService {
             }
             final ShortcutInfoCompat info;
             if (conversation.getMode() == Conversation.MODE_SINGLE) {
-                Contact contact = conversation.getContact();
-                Uri systemAccount = contact.getSystemAccount();
+                final Contact contact = conversation.getContact();
+                final Uri systemAccount = contact.getSystemAccount();
                 if (systemAccount != null) {
                     mBuilder.addPerson(systemAccount.toString());
                 }
@@ -1397,7 +1426,9 @@ public class NotificationService {
 
             mBuilder.setShortcutInfo(info);
             if (Build.VERSION.SDK_INT >= 30) {
-                mXmppConnectionService.getSystemService(ShortcutManager.class).pushDynamicShortcut(info.toShortcutInfo());
+                mXmppConnectionService
+                        .getSystemService(ShortcutManager.class)
+                        .pushDynamicShortcut(info.toShortcutInfo());
                 // mBuilder.setBubbleMetadata(new NotificationCompat.BubbleMetadata.Builder(info.getId()).build());
             }
         }
@@ -1763,51 +1794,26 @@ public class NotificationService {
     }
 
     private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
-        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-        intent.setAction(action);
-        intent.setPackage(mXmppConnectionService.getPackageName());
-        intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
-        return PendingIntent.getService(
-                mXmppConnectionService,
-                requestCode,
-                intent,
-                s()
-                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
-                        : PendingIntent.FLAG_UPDATE_CURRENT);
+        return pendingServiceIntent(mXmppConnectionService, action, requestCode, ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
     }
 
-    private PendingIntent createSnoozeIntent(Conversation conversation) {
-        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-        intent.setAction(XmppConnectionService.ACTION_SNOOZE);
-        intent.putExtra("uuid", conversation.getUuid());
-        intent.setPackage(mXmppConnectionService.getPackageName());
-        return PendingIntent.getService(
-                mXmppConnectionService,
-                generateRequestCode(conversation, 22),
-                intent,
-                s()
-                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
-                        : PendingIntent.FLAG_UPDATE_CURRENT);
+    private PendingIntent createSnoozeIntent(final Conversation conversation) {
+        return pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_SNOOZE, generateRequestCode(conversation,22),ImmutableMap.of("uuid",conversation.getUuid()));
     }
 
-    private PendingIntent createTryAgainIntent() {
-        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-        intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
-        return PendingIntent.getService(
-                mXmppConnectionService,
-                45,
-                intent,
-                s()
-                        ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
-                        : PendingIntent.FLAG_UPDATE_CURRENT);
+    private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode) {
+        return pendingServiceIntent(context, action, requestCode, ImmutableMap.of());
     }
 
-    private PendingIntent createDismissErrorIntent() {
-        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
-        intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
+    private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode, final Map<String,String> extras) {
+        final Intent intent = new Intent(context, XmppConnectionService.class);
+        intent.setAction(action);
+        for(final Map.Entry<String,String> entry : extras.entrySet()) {
+            intent.putExtra(entry.getKey(), entry.getValue());
+        }
         return PendingIntent.getService(
-                mXmppConnectionService,
-                69,
+                context,
+                requestCode,
                 intent,
                 s()
                         ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
@@ -1874,17 +1880,15 @@ public class NotificationService {
         final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
         mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
         final List<Account> accounts = mXmppConnectionService.getAccounts();
-        int enabled = 0;
-        int connected = 0;
-        if (accounts != null) {
-            for (Account account : accounts) {
-                if (account.isOnlineAndConnected()) {
-                    connected++;
-                    enabled++;
-                } else if (account.isEnabled()) {
-                    enabled++;
-                }
-            }
+        final int enabled;
+        final int connected;
+        if (accounts == null) {
+            enabled = 0;
+            connected = 0;
+        } else {
+            enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled));
+            connected =
+                    Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
         }
         mBuilder.setContentText(
                 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
@@ -1902,11 +1906,36 @@ public class NotificationService {
 
         if (Compatibility.runsTwentySix()) {
             mBuilder.setChannelId("foreground");
+            mBuilder.addAction(
+                    R.drawable.ic_logout_white_24dp,
+                    mXmppConnectionService.getString(R.string.log_out),
+                    pendingServiceIntent(
+                            mXmppConnectionService,
+                            XmppConnectionService.ACTION_TEMPORARILY_DISABLE,
+                            87));
+            mBuilder.addAction(
+                    R.drawable.ic_notifications_off_white_24dp,
+                    mXmppConnectionService.getString(R.string.hide_notification),
+                    pendingNotificationSettingsIntent(mXmppConnectionService));
         }
 
         return mBuilder.build();
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private static PendingIntent pendingNotificationSettingsIntent(final Context context) {
+        final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
+        intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
+        intent.putExtra(Settings.EXTRA_CHANNEL_ID, "foreground");
+        return PendingIntent.getActivity(
+                context,
+                89,
+                intent,
+                s()
+                        ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+                        : PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
     private PendingIntent createOpenConversationsIntent() {
         try {
             return PendingIntent.getActivity(
@@ -1957,7 +1986,7 @@ public class NotificationService {
         mBuilder.addAction(
                 R.drawable.ic_autorenew_white_24dp,
                 mXmppConnectionService.getString(R.string.try_again),
-                createTryAgainIntent());
+                pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
         if (torNotAvailable) {
             if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
                 mBuilder.addAction(
@@ -1985,7 +2014,7 @@ public class NotificationService {
                                         : PendingIntent.FLAG_UPDATE_CURRENT));
             }
         }
-        mBuilder.setDeleteIntent(createDismissErrorIntent());
+        mBuilder.setDeleteIntent(pendingServiceIntent(mXmppConnectionService,XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, 69));
         mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
         mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
         mBuilder.setLocalOnly(true);
@@ -2080,6 +2109,11 @@ public class NotificationService {
             lastTime = time;
         }
 
+        public boolean removeMissedCall() {
+            --numberOfCalls;
+            return numberOfCalls <= 0;
+        }
+
         public int getNumberOfCalls() {
             return numberOfCalls;
         }

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

@@ -11,6 +11,7 @@ import android.os.Build;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.graphics.drawable.IconCompat;
 
@@ -102,25 +103,16 @@ public class ShortcutService {
     }
 
     public ShortcutInfoCompat getShortcutInfoCompat(final MucOptions mucOptions) {
-        final ShortcutInfoCompat.Builder builder =
-                new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions))
+        return new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions))
                         .setShortLabel(mucOptions.getConversation().getName())
                         .setIntent(getShortcutIntent(mucOptions))
-                        .setIsConversation();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            builder.setIcon(
-                    IconCompat.createFromIcon(
-                            xmppConnectionService,
-                            Icon.createWithBitmap(
-                                    xmppConnectionService
-                                            .getAvatarService()
-                                            .getRoundedShortcut(mucOptions))));
-        }
-        return builder.build();
+                        .setIcon(IconCompat.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(mucOptions)))
+                        .setIsConversation()
+                        .build();
     }
 
     @TargetApi(Build.VERSION_CODES.N_MR1)
-    private ShortcutInfo getShortcutInfo(Contact contact) {
+    private ShortcutInfo getShortcutInfo(final Contact contact) {
         return getShortcutInfoCompat(contact).toShortcutInfo();
     }
 

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

@@ -19,6 +19,7 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
 import android.graphics.drawable.AnimatedImageDrawable;
@@ -101,6 +102,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Semaphore;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
@@ -144,6 +146,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.persistance.UnifiedPushDatabase;
 import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
+import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.RtpSessionActivity;
 import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.UiCallback;
@@ -211,7 +214,11 @@ public class XmppConnectionService extends Service {
     public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification";
     public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
     public static final String ACTION_TRY_AGAIN = "try_again";
+
+    public static final String ACTION_TEMPORARILY_DISABLE = "temporarily_disable";
+    public static final String ACTION_PING = "ping";
     public static final String ACTION_IDLE_PING = "idle_ping";
+    public static final String ACTION_INTERNAL_PING = "internal_ping";
     public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
     public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
     public static final String ACTION_DISMISS_CALL = "dismiss_call";
@@ -225,6 +232,8 @@ public class XmppConnectionService extends Service {
     public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
     private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
     private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
+
+    private final ScheduledExecutorService internalPingExecutor = Executors.newSingleThreadScheduledExecutor();
     private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression");
     private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
     private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
@@ -488,9 +497,9 @@ public class XmppConnectionService extends Service {
                     joinMuc(conversation);
                 }
                 scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
-            } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
+            } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED || account.getStatus() == Account.State.LOGGED_OUT) {
                 resetSendingToWaiting(account);
-                if (account.isEnabled() && isInLowPingTimeoutMode(account)) {
+                if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) {
                     Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now");
                     reconnectAccount(account, true, false);
                 } else {
@@ -503,7 +512,8 @@ public class XmppConnectionService extends Service {
             } else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) {
                 resetSendingToWaiting(account);
                 if (connection != null && account.getStatus().isAttemptReconnect()) {
-                    final boolean aggressive = hasJingleRtpConnection(account);
+                    final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST
+                            || hasJingleRtpConnection(account);
                     final int next = connection.getTimeToNextAttempt(aggressive);
                     final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
                     if (next <= 0) {
@@ -513,6 +523,13 @@ public class XmppConnectionService extends Service {
                         final int attempt = connection.getAttempt() + 1;
                         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode+", aggressive="+aggressive);
                         scheduleWakeUpCall(next, account.getUuid().hashCode());
+                        if (aggressive) {
+                            internalPingExecutor.schedule(
+                                    XmppConnectionService.this::manageAccountConnectionStatesInternal,
+                                    (next * 1000L) + 50,
+                                    TimeUnit.MILLISECONDS
+                            );
+                        }
                     }
                 }
             }
@@ -524,6 +541,7 @@ public class XmppConnectionService extends Service {
     private WakeLock wakeLock;
     private LruCache<String, Drawable> mDrawableCache;
     private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
+    private final BroadcastReceiver mInternalRestrictedEventReceiver = new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS));
     private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
     private EmojiSearch emojiSearch = null;
 
@@ -797,241 +815,261 @@ public class XmppConnectionService extends Service {
     }
 
     @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        final String action = intent == null ? null : intent.getAction();
+    public int onStartCommand(final Intent intent, int flags, int startId) {
+        final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
         final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
         if (needsForegroundService) {
             Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")");
             toggleForegroundService(true);
         }
-        String pushedAccountHash = null;
-        boolean interactive = false;
-        if (action != null) {
-            final String uuid = intent.getStringExtra("uuid");
-            switch (action) {
-                case QuickConversationsService.SMS_RETRIEVED_ACTION:
-                    mQuickConversationsService.handleSmsReceived(intent);
-                    break;
-                case ConnectivityManager.CONNECTIVITY_ACTION:
-                    if (hasInternetConnection()) {
-                        if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
-                            schedulePostConnectivityChange();
-                        }
-                        if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
-                            resetAllAttemptCounts(true, false);
-                        }
-                        Resolver.clearCache();
+        final String uuid = intent == null ? null : intent.getStringExtra("uuid");
+        switch (action) {
+            case QuickConversationsService.SMS_RETRIEVED_ACTION:
+                mQuickConversationsService.handleSmsReceived(intent);
+                break;
+            case ConnectivityManager.CONNECTIVITY_ACTION:
+                if (hasInternetConnection()) {
+                    if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
+                        schedulePostConnectivityChange();
                     }
-                    break;
-                case Intent.ACTION_SHUTDOWN:
-                    logoutAndSave(true);
-                    return START_NOT_STICKY;
-                case ACTION_CLEAR_MESSAGE_NOTIFICATION:
-                    mNotificationExecutor.execute(() -> {
-                        try {
-                            final Conversation c = findConversationByUuid(uuid);
-                            if (c != null) {
-                                mNotificationService.clearMessages(c);
-                            } else {
-                                mNotificationService.clearMessages();
-                            }
-                            restoredFromDatabaseLatch.await();
-
-                        } catch (InterruptedException e) {
-                            Log.d(Config.LOGTAG, "unable to process clear message notification");
+                    if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
+                        resetAllAttemptCounts(true, false);
+                    }
+                    Resolver.clearCache();
+                }
+                break;
+            case Intent.ACTION_SHUTDOWN:
+                logoutAndSave(true);
+                return START_NOT_STICKY;
+            case ACTION_CLEAR_MESSAGE_NOTIFICATION:
+                mNotificationExecutor.execute(() -> {
+                    try {
+                        final Conversation c = findConversationByUuid(uuid);
+                        if (c != null) {
+                            mNotificationService.clearMessages(c);
+                        } else {
+                            mNotificationService.clearMessages();
                         }
-                    });
-                    break;
-                case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
-                    mNotificationExecutor.execute(() -> {
-                        try {
-                            final Conversation c = findConversationByUuid(uuid);
-                            if (c != null) {
-                                mNotificationService.clearMissedCalls(c);
-                            } else {
-                                mNotificationService.clearMissedCalls();
-                            }
-                            restoredFromDatabaseLatch.await();
+                        restoredFromDatabaseLatch.await();
 
-                        } catch (InterruptedException e) {
-                            Log.d(Config.LOGTAG, "unable to process clear missed call notification");
+                    } catch (InterruptedException e) {
+                        Log.d(Config.LOGTAG, "unable to process clear message notification");
+                    }
+                });
+                break;
+            case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
+                mNotificationExecutor.execute(() -> {
+                    try {
+                        final Conversation c = findConversationByUuid(uuid);
+                        if (c != null) {
+                            mNotificationService.clearMissedCalls(c);
+                        } else {
+                            mNotificationService.clearMissedCalls();
                         }
-                    });
-                    break;
-                case ACTION_DISMISS_CALL: {
-                    final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
-                    Log.d(Config.LOGTAG, "received intent to dismiss call with session id " + sessionId);
-                    mJingleConnectionManager.rejectRtpSession(sessionId);
-                    break;
-                }
-                case TorServiceUtils.ACTION_STATUS:
-                    final String status = intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
-                    //TODO port and host are in 'extras' - but this may not be a reliable source?
-                    if ("ON".equals(status)) {
-                        handleOrbotStartedEvent();
-                        return START_STICKY;
+                        restoredFromDatabaseLatch.await();
+
+                    } catch (InterruptedException e) {
+                        Log.d(Config.LOGTAG, "unable to process clear missed call notification");
                     }
+                });
+                break;
+            case ACTION_DISMISS_CALL: {
+                if (intent == null) {
                     break;
-                case ACTION_END_CALL: {
-                    final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
-                    Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId);
-                    mJingleConnectionManager.endRtpSession(sessionId);
                 }
+                final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
+                Log.d(Config.LOGTAG, "received intent to dismiss call with session id " + sessionId);
+                mJingleConnectionManager.rejectRtpSession(sessionId);
                 break;
-                case ACTION_PROVISION_ACCOUNT: {
-                    final String address = intent.getStringExtra("address");
-                    final String password = intent.getStringExtra("password");
-                    if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) {
-                        break;
-                    }
-                    provisionAccount(address, password);
+            }
+            case TorServiceUtils.ACTION_STATUS:
+                final String status = intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
+                //TODO port and host are in 'extras' - but this may not be a reliable source?
+                if ("ON".equals(status)) {
+                    handleOrbotStartedEvent();
+                    return START_STICKY;
+                }
+                break;
+            case ACTION_END_CALL: {
+                if (intent == null) {
                     break;
                 }
-                case ACTION_DISMISS_ERROR_NOTIFICATIONS:
-                    dismissErrorNotifications();
+                final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
+                Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId);
+                mJingleConnectionManager.endRtpSession(sessionId);
+            }
+            break;
+            case ACTION_PROVISION_ACCOUNT: {
+                if (intent == null) {
                     break;
-                case ACTION_TRY_AGAIN:
-                    resetAllAttemptCounts(false, true);
-                    interactive = true;
+                }
+                final String address = intent.getStringExtra("address");
+                final String password = intent.getStringExtra("password");
+                if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) {
                     break;
-                case ACTION_REPLY_TO_CONVERSATION:
-                    Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
-                    if (remoteInput == null) {
-                        break;
-                    }
-                    final CharSequence body = remoteInput.getCharSequence("text_reply");
-                    final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
-                    final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
-                    if (body == null || body.length() <= 0) {
-                        break;
-                    }
-                    mNotificationExecutor.execute(() -> {
-                        try {
-                            restoredFromDatabaseLatch.await();
-                            final Conversation c = findConversationByUuid(uuid);
-                            if (c != null) {
-                                directReply(c, body.toString(), lastMessageUuid, dismissNotification);
-                            }
-                        } catch (InterruptedException e) {
-                            Log.d(Config.LOGTAG, "unable to process direct reply");
-                        }
-                    });
+                }
+                provisionAccount(address, password);
+                break;
+            }
+            case ACTION_DISMISS_ERROR_NOTIFICATIONS:
+                dismissErrorNotifications();
+                break;
+            case ACTION_TRY_AGAIN:
+                resetAllAttemptCounts(false, true);
+                break;
+            case ACTION_REPLY_TO_CONVERSATION:
+                final Bundle remoteInput = intent == null ? null : RemoteInput.getResultsFromIntent(intent);
+                if (remoteInput == null) {
                     break;
-                case ACTION_MARK_AS_READ:
-                    mNotificationExecutor.execute(() -> {
-                        final Conversation c = findConversationByUuid(uuid);
-                        if (c == null) {
-                            Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")");
-                            return;
-                        }
-                        try {
-                            restoredFromDatabaseLatch.await();
-                            sendReadMarker(c, null);
-                        } catch (InterruptedException e) {
-                            Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName());
-                        }
-
-                    });
+                }
+                final CharSequence body = remoteInput.getCharSequence("text_reply");
+                final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
+                final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
+                if (body == null || body.length() <= 0) {
                     break;
-                case ACTION_SNOOZE:
-                    mNotificationExecutor.execute(() -> {
+                }
+                mNotificationExecutor.execute(() -> {
+                    try {
+                        restoredFromDatabaseLatch.await();
                         final Conversation c = findConversationByUuid(uuid);
-                        if (c == null) {
-                            Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")");
-                            return;
+                        if (c != null) {
+                            directReply(c, body.toString(), lastMessageUuid, dismissNotification);
                         }
-                        c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
-                        mNotificationService.clearMessages(c);
-                        updateConversation(c);
-                    });
-                case AudioManager.RINGER_MODE_CHANGED_ACTION:
-                case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
-                    if (dndOnSilentMode()) {
-                        refreshAllPresences();
+                    } catch (InterruptedException e) {
+                        Log.d(Config.LOGTAG, "unable to process direct reply");
                     }
-                    break;
-                case Intent.ACTION_SCREEN_ON:
-                    deactivateGracePeriod();
-                case Intent.ACTION_USER_PRESENT:
-                case Intent.ACTION_SCREEN_OFF:
-                    if (awayWhenScreenLocked()) {
-                        refreshAllPresences();
-                    }
-                    break;
-                case ACTION_FCM_TOKEN_REFRESH:
-                    refreshAllFcmTokens();
-                    break;
-                case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
-                    final String instance = intent.getStringExtra("instance");
-                    final String application = intent.getStringExtra("application");
-                    final Messenger messenger = intent.getParcelableExtra("messenger");
-                    final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger;
-                    if (messenger != null && application != null && instance != null) {
-                        pushTargetMessenger = new UnifiedPushBroker.PushTargetMessenger(new UnifiedPushDatabase.PushTarget(application, instance),messenger);
-                        Log.d(Config.LOGTAG,"found push target messenger");
-                    } else {
-                        pushTargetMessenger = null;
+                });
+                break;
+            case ACTION_MARK_AS_READ:
+                mNotificationExecutor.execute(() -> {
+                    final Conversation c = findConversationByUuid(uuid);
+                    if (c == null) {
+                        Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")");
+                        return;
                     }
-                    final Optional<UnifiedPushBroker.Transport> transport = renewUnifiedPushEndpoints(pushTargetMessenger);
-                    if (instance != null && transport.isPresent()) {
-                        unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get());
+                    try {
+                        restoredFromDatabaseLatch.await();
+                        sendReadMarker(c, null);
+                    } catch (InterruptedException e) {
+                        Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName());
                     }
-                    break;
-                case ACTION_IDLE_PING:
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                        scheduleNextIdlePing();
+
+                });
+                break;
+            case ACTION_SNOOZE:
+                mNotificationExecutor.execute(() -> {
+                    final Conversation c = findConversationByUuid(uuid);
+                    if (c == null) {
+                        Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")");
+                        return;
                     }
+                    c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
+                    mNotificationService.clearMessages(c);
+                    updateConversation(c);
+                });
+            case AudioManager.RINGER_MODE_CHANGED_ACTION:
+            case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
+                if (dndOnSilentMode()) {
+                    refreshAllPresences();
+                }
+                break;
+            case Intent.ACTION_SCREEN_ON:
+                deactivateGracePeriod();
+            case Intent.ACTION_USER_PRESENT:
+            case Intent.ACTION_SCREEN_OFF:
+                if (awayWhenScreenLocked()) {
+                    refreshAllPresences();
+                }
+                break;
+            case ACTION_FCM_TOKEN_REFRESH:
+                refreshAllFcmTokens();
+                break;
+            case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
+                if (intent == null) {
                     break;
-                case ACTION_FCM_MESSAGE_RECEIVED:
-                    pushedAccountHash = intent.getStringExtra("account");
-                    Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash);
-                    break;
-                case Intent.ACTION_SEND:
-                    Uri uri = intent.getData();
-                    if (uri != null) {
-                        Log.d(Config.LOGTAG, "received uri permission for " + uri);
-                    }
-                    return START_STICKY;
-            }
-        }
-        synchronized (this) {
-            WakeLockHelper.acquire(wakeLock);
-            boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
-            final HashSet<Account> pingCandidates = new HashSet<>();
-            final String androidId = PhoneHelper.getAndroidId(this);
-            for (Account account : accounts) {
-                final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash);
-                pingNow |= processAccountState(account,
-                        interactive,
-                        "ui".equals(action),
-                        pushWasMeantForThisAccount,
-                        pingCandidates);
-            }
-            if (pingNow) {
-                for (Account account : pingCandidates) {
-                    final boolean lowTimeout = isInLowPingTimeoutMode(account);
-                    account.getXmppConnection().sendPing();
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")");
-                    scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
-                }
-                long msToMucPing = (mLastMucPing + (Config.PING_MAX_INTERVAL * 2000L)) - SystemClock.elapsedRealtime();
-                if (msToMucPing <= 0) {
-                    mLastMucPing = SystemClock.elapsedRealtime();
-                    for (Conversation c : getConversations()) {
-                        if (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().online()) {
-                            mucSelfPingAndRejoin(c);
-                        }
-                    }
                 }
-            }
-            WakeLockHelper.release(wakeLock);
+                final String instance = intent.getStringExtra("instance");
+                final String application = intent.getStringExtra("application");
+                final Messenger messenger = intent.getParcelableExtra("messenger");
+                final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger;
+                if (messenger != null && application != null && instance != null) {
+                    pushTargetMessenger = new UnifiedPushBroker.PushTargetMessenger(new UnifiedPushDatabase.PushTarget(application, instance),messenger);
+                    Log.d(Config.LOGTAG,"found push target messenger");
+                } else {
+                    pushTargetMessenger = null;
+                }
+                final Optional<UnifiedPushBroker.Transport> transport = renewUnifiedPushEndpoints(pushTargetMessenger);
+                if (instance != null && transport.isPresent()) {
+                    unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get());
+                }
+                break;
+            case ACTION_IDLE_PING:
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    scheduleNextIdlePing();
+                }
+                break;
+            case ACTION_FCM_MESSAGE_RECEIVED:
+                Log.d(Config.LOGTAG, "push message arrived in service. account");
+                break;
+            case Intent.ACTION_SEND:
+                final Uri uri = intent == null ? null : intent.getData();
+                if (uri != null) {
+                    Log.d(Config.LOGTAG, "received uri permission for " + uri);
+                }
+                return START_STICKY;
+            case ACTION_TEMPORARILY_DISABLE:
+                toggleSoftDisabled(true);
+                if (checkListeners()) {
+                    stopSelf();
+                }
+                return START_NOT_STICKY;
         }
+        manageAccountConnectionStates(action, intent == null ? null : intent.getExtras());
         if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
             expireOldMessages();
         }
         return START_STICKY;
     }
 
+    private void manageAccountConnectionStatesInternal() {
+        manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
+    }
+
+    private synchronized void manageAccountConnectionStates(final String action, final Bundle extras) {
+        final String pushedAccountHash = extras == null ? null : extras.getString("account");
+        final boolean interactive = Arrays.asList(ACTION_TRY_AGAIN).contains(action);
+        WakeLockHelper.acquire(wakeLock);
+        boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
+        final HashSet<Account> pingCandidates = new HashSet<>();
+        final String androidId = PhoneHelper.getAndroidId(this);
+        for (final Account account : accounts) {
+            final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash);
+            pingNow |= processAccountState(account,
+                    interactive,
+                    "ui".equals(action),
+                    pushWasMeantForThisAccount,
+                    pingCandidates);
+        }
+        if (pingNow) {
+            for (Account account : pingCandidates) {
+                final boolean lowTimeout = isInLowPingTimeoutMode(account);
+                account.getXmppConnection().sendPing();
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")");
+                scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
+            }
+            long msToMucPing = (mLastMucPing + (Config.PING_MAX_INTERVAL * 2000L)) - SystemClock.elapsedRealtime();
+            if (msToMucPing <= 0) {
+                mLastMucPing = SystemClock.elapsedRealtime();
+                for (Conversation c : getConversations()) {
+                    if (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().online()) {
+                        mucSelfPingAndRejoin(c);
+                    }
+                }
+            }
+        }
+        WakeLockHelper.release(wakeLock);
+    }
+
     private void handleOrbotStartedEvent() {
         for (final Account account : accounts) {
             if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) {
@@ -1041,79 +1079,85 @@ public class XmppConnectionService extends Service {
     }
 
     private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
-        boolean pingNow = false;
-        if (account.getStatus().isAttemptReconnect()) {
-            if (!hasInternetConnection()) {
-                account.setStatus(Account.State.NO_INTERNET);
-                if (statusListener != null) {
-                    statusListener.onStatusChanged(account);
-                }
-            } else {
-                if (account.getStatus() == Account.State.NO_INTERNET) {
-                    account.setStatus(Account.State.OFFLINE);
-                    if (statusListener != null) {
-                        statusListener.onStatusChanged(account);
-                    }
-                }
-                if (account.getStatus() == Account.State.ONLINE) {
-                    synchronized (mLowPingTimeoutMode) {
-                        long lastReceived = account.getXmppConnection().getLastPacketReceived();
-                        long lastSent = account.getXmppConnection().getLastPingSent();
-                        long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
-                        long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
-                        int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
-                        long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
-                        if (lastSent > lastReceived) {
-                            if (pingTimeoutIn < 0) {
-                                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
-                                this.reconnectAccount(account, true, interactive);
-                            } else {
-                                int secs = (int) (pingTimeoutIn / 1000);
-                                this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
+        if (!account.getStatus().isAttemptReconnect()) {
+            return false;
+        }
+        if (!hasInternetConnection()) {
+            account.setStatus(Account.State.NO_INTERNET);
+            statusListener.onStatusChanged(account);
+        } else {
+            if (account.getStatus() == Account.State.NO_INTERNET) {
+                account.setStatus(Account.State.OFFLINE);
+                statusListener.onStatusChanged(account);
+            }
+            if (account.getStatus() == Account.State.ONLINE) {
+                synchronized (mLowPingTimeoutMode) {
+                    long lastReceived = account.getXmppConnection().getLastPacketReceived();
+                    long lastSent = account.getXmppConnection().getLastPingSent();
+                    long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
+                    long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
+                    int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
+                    long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
+                    if (lastSent > lastReceived) {
+                        if (pingTimeoutIn < 0) {
+                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
+                            this.reconnectAccount(account, true, interactive);
+                        } else {
+                            int secs = (int) (pingTimeoutIn / 1000);
+                            this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
+                        }
+                    } else {
+                        pingCandidates.add(account);
+                        if (isAccountPushed) {
+                            if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
+                                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode");
                             }
+                            return true;
+                        } else if (msToNextPing <= 0) {
+                            return true;
                         } else {
-                            pingCandidates.add(account);
-                            if (isAccountPushed) {
-                                pingNow = true;
-                                if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
-                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode");
-                                }
-                            } else if (msToNextPing <= 0) {
-                                pingNow = true;
-                            } else {
-                                this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
-                                if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
-                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
-                                }
+                            this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
+                            if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
+                                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
                             }
                         }
                     }
-                } else if (account.getStatus() == Account.State.OFFLINE) {
+                }
+            } else if (account.getStatus() == Account.State.OFFLINE) {
+                reconnectAccount(account, true, interactive);
+            } else if (account.getStatus() == Account.State.CONNECTING) {
+                long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
+                long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
+                long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
+                long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
+                if (timeout < 0) {
+                    Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")");
+                    account.getXmppConnection().resetAttemptCount(false);
                     reconnectAccount(account, true, interactive);
-                } else if (account.getStatus() == Account.State.CONNECTING) {
-                    long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
-                    long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
-                    long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
-                    long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
-                    if (timeout < 0) {
-                        Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")");
-                        account.getXmppConnection().resetAttemptCount(false);
-                        reconnectAccount(account, true, interactive);
-                    } else if (discoTimeout < 0) {
-                        account.getXmppConnection().sendDiscoTimeout();
-                        scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
-                    } else {
-                        scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
-                    }
+                } else if (discoTimeout < 0) {
+                    account.getXmppConnection().sendDiscoTimeout();
+                    scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
                 } else {
-                    final boolean aggressive = hasJingleRtpConnection(account);
-                    if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) {
-                        reconnectAccount(account, true, interactive);
-                    }
+                    scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
+                }
+            } else {
+                final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST || hasJingleRtpConnection(account);
+                if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) {
+                    reconnectAccount(account, true, interactive);
+                }
+            }
+        }
+        return false;
+    }
+
+    private void toggleSoftDisabled(final boolean softDisabled) {
+        for(final Account account : this.accounts) {
+            if (account.isEnabled()) {
+                if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) {
+                    updateAccount(account);
                 }
             }
         }
-        return pingNow;
     }
 
     public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) {
@@ -1395,7 +1439,7 @@ public class XmppConnectionService extends Service {
         if (Config.supportOpenPgp()) {
             this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
                 @Override
-                public void onBound(IOpenPgpService2 service) {
+                public void onBound(final IOpenPgpService2 service) {
                     for (Account account : accounts) {
                         final PgpDecryptionService pgp = account.getPgpDecryptionService();
                         if (pgp != null) {
@@ -1405,7 +1449,8 @@ public class XmppConnectionService extends Service {
                 }
 
                 @Override
-                public void onError(Exception e) {
+                public void onError(final Exception exception) {
+                    Log.e(Config.LOGTAG,"could not bind to OpenKeyChain", exception);
                 }
             });
             this.pgpServiceConnection.bindToService();
@@ -1417,20 +1462,31 @@ public class XmppConnectionService extends Service {
         toggleForegroundService();
         updateUnreadCountBadge();
         toggleScreenEventReceiver();
-        final IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(TorServiceUtils.ACTION_STATUS);
+        final IntentFilter systemBroadcastFilter = new IntentFilter();
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             scheduleNextIdlePing();
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
-            }
-            intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
-        }
-        registerReceiver(this.mInternalEventReceiver, intentFilter);
+                systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+            }
+            systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
+        }
+        ContextCompat.registerReceiver(
+                this,
+                this.mInternalEventReceiver,
+                systemBroadcastFilter,
+                ContextCompat.RECEIVER_NOT_EXPORTED);
+        final IntentFilter exportedBroadcastFilter = new IntentFilter();
+        exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS);
+        ContextCompat.registerReceiver(
+                this,
+                this.mInternalRestrictedEventReceiver,
+                exportedBroadcastFilter,
+                ContextCompat.RECEIVER_EXPORTED);
         mForceDuringOnCreate.set(false);
         toggleForegroundService();
         setupPhoneStateListener();
         rescanStickers();
+        internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS);
     }
 
 
@@ -1497,12 +1553,14 @@ public class XmppConnectionService extends Service {
     public void onDestroy() {
         try {
             unregisterReceiver(this.mInternalEventReceiver);
+            unregisterReceiver(this.mInternalRestrictedEventReceiver);
             unregisterReceiver(this.mInternalScreenEventReceiver);
         } catch (final IllegalArgumentException e) {
             //ignored
         }
         destroyed = false;
         fileObserver.stopWatching();
+        internalPingExecutor.shutdown();
         super.onDestroy();
     }
 
@@ -1579,9 +1637,27 @@ public class XmppConnectionService extends Service {
 
     private void startForegroundOrCatch(final int id, final Notification notification) {
         try {
-            startForeground(id, notification);
-        } catch (final IllegalStateException e) {
-            Log.e(Config.LOGTAG,"Could not start foreground service", e);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                final int foregroundServiceType;
+                if (getSystemService(PowerManager.class)
+                        .isIgnoringBatteryOptimizations(getPackageName())) {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
+                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+                } else {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
+                    Log.w(Config.LOGTAG,"falling back to special use foreground service type");
+                }
+                startForeground(id, notification, foregroundServiceType);
+            } else {
+                startForeground(id, notification);
+            }
+        } catch (final IllegalStateException | SecurityException e) {
+            Log.e(Config.LOGTAG, "Could not start foreground service", e);
         }
     }
 
@@ -1602,7 +1678,7 @@ public class XmppConnectionService extends Service {
     private void logoutAndSave(boolean stop) {
         int activeAccounts = 0;
         for (final Account account : accounts) {
-            if (account.getStatus() != Account.State.DISABLED) {
+            if (account.isConnectionEnabled()) {
                 databaseBackend.writeRoster(account.getRoster());
                 activeAccounts++;
             }
@@ -1638,25 +1714,18 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void scheduleWakeUpCall(int seconds, int requestCode) {
+    public void scheduleWakeUpCall(final int seconds, final int requestCode) {
         final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L;
         final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
         if (alarmManager == null) {
             return;
         }
         final Intent intent = new Intent(this, EventReceiver.class);
-        intent.setAction("ping");
+        intent.setAction(ACTION_PING);
         try {
-            final PendingIntent pendingIntent;
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                pendingIntent =
-                        PendingIntent.getBroadcast(
-                                this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
-            } else {
-                pendingIntent =
-                        PendingIntent.getBroadcast(
-                                this, requestCode, intent, 0);
-            }
+            final PendingIntent pendingIntent =
+                    PendingIntent.getBroadcast(
+                            this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
             alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
         } catch (RuntimeException e) {
             Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
@@ -3120,6 +3189,7 @@ public class XmppConnectionService extends Service {
     }
 
     private void switchToForeground() {
+        toggleSoftDisabled(false);
         final boolean broadcastLastActivity = broadcastLastActivity();
         for (Conversation conversation : getConversations()) {
             if (conversation.getMode() == Conversation.MODE_MULTI) {
@@ -3500,8 +3570,8 @@ public class XmppConnectionService extends Service {
         if (this.accounts == null) {
             return false;
         }
-        for (Account account : this.accounts) {
-            if (account.isEnabled()) {
+        for (final Account account : this.accounts) {
+            if (account.isConnectionEnabled()) {
                 return true;
             }
         }
@@ -3948,23 +4018,23 @@ public class XmppConnectionService extends Service {
         });
     }
 
-    private void disconnect(Account account, boolean force) {
-        if ((account.getStatus() == Account.State.ONLINE)
-                || (account.getStatus() == Account.State.DISABLED)) {
-            final XmppConnection connection = account.getXmppConnection();
-            if (!force) {
-                List<Conversation> conversations = getConversations();
-                for (Conversation conversation : conversations) {
-                    if (conversation.getAccount() == account) {
-                        if (conversation.getMode() == Conversation.MODE_MULTI) {
-                            leaveMuc(conversation, true);
-                        }
+    private void disconnect(final Account account, boolean force) {
+        final XmppConnection connection = account.getXmppConnection();
+        if (connection == null) {
+            return;
+        }
+        if (!force) {
+            final List<Conversation> conversations = getConversations();
+            for (Conversation conversation : conversations) {
+                if (conversation.getAccount() == account) {
+                    if (conversation.getMode() == Conversation.MODE_MULTI) {
+                        leaveMuc(conversation, true);
                     }
                 }
-                sendOfflinePresence(account);
             }
-            connection.disconnect(force);
+            sendOfflinePresence(account);
         }
+        connection.disconnect(force);
     }
 
     @Override
@@ -4485,13 +4555,18 @@ public class XmppConnectionService extends Service {
 
     private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
         synchronized (account) {
-            XmppConnection connection = account.getXmppConnection();
-            if (connection == null) {
+            final XmppConnection existingConnection = account.getXmppConnection();
+            final XmppConnection connection;
+            if (existingConnection != null) {
+                connection = existingConnection;
+            } else if (account.isConnectionEnabled()) {
                 connection = createConnection(account);
                 account.setXmppConnection(connection);
+            } else {
+                return;
             }
-            boolean hasInternet = hasInternetConnection();
-            if (account.isEnabled() && hasInternet) {
+            final boolean hasInternet = hasInternetConnection();
+            if (account.isConnectionEnabled() && hasInternet) {
                 if (!force) {
                     disconnect(account, false);
                 }
@@ -4989,7 +5064,7 @@ public class XmppConnectionService extends Service {
     public void refreshAllPresences() {
         boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
         for (Account account : getAccounts()) {
-            if (account.isEnabled()) {
+            if (account.isConnectionEnabled()) {
                 sendPresence(account, includeIdleTimestamp);
             }
         }
@@ -5532,11 +5607,30 @@ public class XmppConnectionService extends Service {
     private class InternalEventReceiver extends BroadcastReceiver {
 
         @Override
-        public void onReceive(Context context, Intent intent) {
+        public void onReceive(final Context context, final Intent intent) {
             onStartCommand(intent, 0, 0);
         }
     }
 
+    private class RestrictedEventReceiver extends BroadcastReceiver {
+
+        private final Collection<String> allowedActions;
+
+        private RestrictedEventReceiver(final Collection<String> allowedActions) {
+            this.allowedActions = allowedActions;
+        }
+
+        @Override
+        public void onReceive(final Context context, final Intent intent) {
+            final String action = intent == null ? null : intent.getAction();
+            if (allowedActions.contains(action)) {
+                onStartCommand(intent,0,0);
+            } else {
+                Log.e(Config.LOGTAG,"restricting broadcast of event "+action);
+            }
+        }
+    }
+
     public static class OngoingCall {
         public final AbstractJingleConnection.Id id;
         public final Set<Media> media;
@@ -5562,5 +5656,19 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    public static void toggleForegroundService(final XmppConnectionService service) {
+        if (service == null) {
+            return;
+        }
+        service.toggleForegroundService();
+    }
+
+    public static void toggleForegroundService(final ConversationsActivity activity) {
+        if (activity == null) {
+            return;
+        }
+        toggleForegroundService(activity.xmppConnectionService);
+    }
+
     public static class BlockedMediaException extends Exception { }
 }

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

@@ -279,7 +279,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
             return;
         }
         for (final Account account : xmppConnectionService.getAccounts()) {
-            if (account.getStatus() != Account.State.DISABLED) {
+            if (account.isEnabled()) {
                 for (final Contact contact : account.getRoster().getContacts()) {
                     if (contact.showInContactList() &&
                             !filterContacts.contains(contact.getJid().asBareJid().toString())
@@ -382,7 +382,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
         filterContacts();
         this.mActivatedAccounts.clear();
         for (Account account : xmppConnectionService.getAccounts()) {
-            if (account.getStatus() != Account.State.DISABLED) {
+            if (account.isEnabled()) {
                 if (Config.DOMAIN_LOCK != null) {
                     this.mActivatedAccounts.add(account.getJid().getEscapedLocal());
                 } else {
@@ -402,6 +402,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
 
     @Override
     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
     }
 

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

@@ -153,7 +153,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             builder.setMultiChoiceItems(configuration.names, values, (dialog, which, isChecked) -> values[which] = isChecked);
             builder.setNegativeButton(R.string.cancel, null);
             builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
-                Bundle options = configuration.toBundle(values);
+                final Bundle options = configuration.toBundle(values);
                 options.putString("muc#roomconfig_persistentroom", "1");
                 options.putString("{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", options.getString("muc#roomconfig_allowinvites"));
                 xmppConnectionService.pushConferenceConfiguration(mConversation,

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

@@ -543,6 +543,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             }
             boolean skippedInactive = false;
             boolean showsInactive = false;
+            boolean showUnverifiedWarning = false;
             for (final XmppAxolotlSession session : sessions) {
                 final FingerprintStatus trust = session.getTrust();
                 hasKeys |= !trust.isCompromised();
@@ -558,7 +559,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                     boolean highlight = session.getFingerprint().equals(messageFingerprint);
                     addFingerprintRow(binding.detailsContactKeys, session, highlight);
                 }
+                if (trust.isUnverified()) {
+                    showUnverifiedWarning = true;
+                }
             }
+            binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
             if (showsInactive || skippedInactive) {
                 binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
                 binding.showInactiveDevices.setVisibility(View.VISIBLE);
@@ -568,7 +573,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         } else {
             binding.showInactiveDevices.setVisibility(View.GONE);
         }
-        binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable() ? View.VISIBLE : View.GONE);
+        final boolean isCameraFeatureAvailable = isCameraFeatureAvailable();
+        binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
         if (hasKeys) {
             binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
         }

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

@@ -4,6 +4,8 @@ import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
 import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
 import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
 import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.audioGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.cameraGranted;
 import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
 import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
 
@@ -187,6 +189,19 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
 public class ConversationFragment extends XmppFragment
         implements EditMessage.KeyboardListener,
                 MessageAdapter.OnContactPictureLongClicked,
@@ -468,6 +483,7 @@ public class ConversationFragment extends XmppFragment
                 public void onClick(View v) {
                     final Account account = conversation == null ? null : conversation.getAccount();
                     if (account != null) {
+                        account.setOption(Account.OPTION_SOFT_DISABLED, false);
                         account.setOption(Account.OPTION_DISABLED, false);
                         activity.xmppConnectionService.updateAccount(account);
                     }
@@ -530,7 +546,8 @@ public class ConversationFragment extends XmppFragment
                                             null,
                                             0,
                                             0,
-                                            0);
+                                            0,
+                                            Compatibility.pgpStartIntentSenderOptions());
                         } catch (SendIntentException e) {
                             Toast.makeText(
                                             getActivity(),
@@ -2075,6 +2092,10 @@ public class ConversationFragment extends XmppFragment
                     .show();
             return;
         }
+        final Account account = conversation.getAccount();
+        if (account.setOption(Account.OPTION_SOFT_DISABLED, false)) {
+            activity.xmppConnectionService.updateAccount(account);
+        }
         final Contact contact = conversation.getContact();
         if (RtpCapability.jmiSupport(contact)) {
             triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
@@ -2331,6 +2352,9 @@ public class ConversationFragment extends XmppFragment
             }
             refresh();
         }
+        if (cameraGranted(grantResults, permissions) || audioGranted(grantResults, permissions)) {
+            XmppConnectionService.toggleForegroundService(activity);
+        }
     }
 
     public void startDownloadable(Message message) {
@@ -3362,6 +3386,8 @@ public class ConversationFragment extends XmppFragment
                     R.string.this_account_is_disabled,
                     R.string.enable,
                     this.mEnableAccountListener);
+        } else if (account.getStatus() == Account.State.LOGGED_OUT) {
+            showSnackbar(R.string.this_account_is_logged_out,R.string.log_in,this.mEnableAccountListener);
         } else if (conversation.isBlocked()) {
             showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
         } else if (contact != null
@@ -4114,7 +4140,7 @@ public class ConversationFragment extends XmppFragment
         try {
             getActivity()
                     .startIntentSenderForResult(
-                            pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0);
+                            pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
         } catch (final SendIntentException ignored) {
         }
     }

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

@@ -72,8 +72,9 @@ import io.michaelrocks.libphonenumber.android.NumberParseException;
 import org.openintents.openpgp.util.OpenPgpApi;
 
 import java.util.Arrays;
-import java.util.List;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -439,14 +440,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         }
     }
 
-    private void handleActivityResult(ActivityResult activityResult) {
+    private void handleActivityResult(final ActivityResult activityResult) {
         if (activityResult.resultCode == Activity.RESULT_OK) {
             handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
         } else {
             handleNegativeActivityResult(activityResult.requestCode);
         }
         if (activityResult.requestCode == REQUEST_BATTERY_OP) {
+            // the result code is always 0 even when battery permission were granted
             requestNotificationPermissionIfNeeded();
+            XmppConnectionService.toggleForegroundService(xmppConnectionService);
         }
     }
 

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

@@ -54,6 +54,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 import eu.siacs.conversations.databinding.ActivityEditAccountBinding;
 import eu.siacs.conversations.databinding.DialogPresenceBinding;
@@ -71,6 +72,7 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
+import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.Resolver;
 import eu.siacs.conversations.utils.SignupUtils;
@@ -161,7 +163,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             if (mInitMode && mAccount != null) {
                 mAccount.setOption(Account.OPTION_DISABLED, false);
             }
-            if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited) {
+            if (mAccount != null && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT).contains(mAccount.getStatus()) && !accountInfoEdited) {
+                mAccount.setOption(Account.OPTION_SOFT_DISABLED, false);
                 mAccount.setOption(Account.OPTION_DISABLED, false);
                 if (!xmppConnectionService.updateAccount(mAccount)) {
                     Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
@@ -481,6 +484,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
             updateAccountInformation(mAccount == null);
         }
+        if (requestCode == REQUEST_BATTERY_OP) {
+            // the result code is always 0 even when battery permission were granted
+            XmppConnectionService.toggleForegroundService(xmppConnectionService);
+        }
         if (requestCode == REQUEST_CHANGE_STATUS) {
             PresenceTemplate template = mPendingPresenceTemplate.pop();
             if (template != null && resultCode == Activity.RESULT_OK) {
@@ -652,6 +659,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             intent.putExtra("suffix", ":" + mAccount.getUuid());
             startActivity(intent);
         });
+        this.binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
     }
 
     private void onEditYourNameClicked(View view) {
@@ -1021,7 +1029,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             public void userInputRequired(PendingIntent pi, String object) {
                 mPendingPresenceTemplate.push(template);
                 try {
-                    startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0);
+                    startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
                 } catch (final IntentSender.SendIntentException ignored) {
                 }
             }
@@ -1225,19 +1233,24 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                     this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint);
                 }
                 this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2)));
-                this.binding.actionCopyAxolotlToClipboard.setVisibility(View.VISIBLE);
-                this.binding.actionCopyAxolotlToClipboard.setOnClickListener(v -> copyOmemoFingerprint(ownAxolotlFingerprint));
+                this.binding.showQrCodeButton.setVisibility(View.VISIBLE);
+                this.binding.showQrCodeButton.setOnClickListener(v -> showQrCode());
             } else {
                 this.binding.axolotlFingerprintBox.setVisibility(View.GONE);
             }
             boolean hasKeys = false;
+            boolean showUnverifiedWarning = false;
             binding.otherDeviceKeys.removeAllViews();
-            for (XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
-                if (!session.getTrust().isCompromised()) {
+            for (final XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
+                final FingerprintStatus trust = session.getTrust();
+                if (!trust.isCompromised()) {
                     boolean highlight = session.getFingerprint().equals(messageFingerprint);
                     addFingerprintRow(binding.otherDeviceKeys, session, highlight);
                     hasKeys = true;
                 }
+                if (trust.isUnverified()) {
+                    showUnverifiedWarning = true;
+                }
             }
             if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices
                 this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE);
@@ -1247,6 +1260,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                 } else {
                     binding.clearDevices.setVisibility(View.VISIBLE);
                 }
+                binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
+                binding.scanButton.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
             } else {
                 this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
             }

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

@@ -13,8 +13,13 @@ import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Locale;
@@ -56,30 +61,38 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ
     private void loadAndSubmitUsers() {
         if (mConversation != null) {
             allUsers = mConversation.getMucOptions().getUsers();
-            Collections.sort(allUsers);
             submitFilteredList(mSearchEditText != null ? mSearchEditText.getText().toString() : null);
         }
     }
 
-    private void submitFilteredList(String search) {
+    private void submitFilteredList(final String search) {
         if (TextUtils.isEmpty(search)) {
-            userAdapter.submitList(allUsers);
+            userAdapter.submitList(Ordering.natural().immutableSortedCopy(allUsers));
         } else {
             final String needle = search.toLowerCase(Locale.getDefault());
-            ArrayList<MucOptions.User> filtered = new ArrayList<>();
-            for(MucOptions.User user : allUsers) {
-                final String name = user.getNick();
-                final Contact contact = user.getContact();
-                if (name != null && name.toLowerCase(Locale.getDefault()).contains(needle) || contact != null && contact.getDisplayName().toLowerCase(Locale.getDefault()).contains(needle)) {
-                    filtered.add(user);
-                }
-            }
-            userAdapter.submitList(filtered);
+            userAdapter.submitList(
+                    Ordering.natural()
+                            .immutableSortedCopy(
+                                    Collections2.filter(
+                                            this.allUsers,
+                                            user -> {
+                                                final String name = user.getName();
+                                                final Contact contact = user.getContact();
+                                                return name != null
+                                                                && name.toLowerCase(
+                                                                                Locale.getDefault())
+                                                                        .contains(needle)
+                                                        || contact != null
+                                                                && contact.getDisplayName()
+                                                                        .toLowerCase(
+                                                                                Locale.getDefault())
+                                                                        .contains(needle);
+                                            })));
         }
     }
 
     @Override
-    public boolean onContextItemSelected(MenuItem item) {
+    public boolean onContextItemSelected(@NonNull MenuItem item) {
         if (!MucDetailsContextMenuHelper.onContextItemSelected(item, userAdapter.getSelectedUser(), this)) {
             return super.onContextItemSelected(item);
         }

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

@@ -205,6 +205,7 @@ public abstract class OmemoActivity extends XmppActivity {
 
 	@Override
 	public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 		ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
 	}
 }

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

@@ -98,18 +98,20 @@ public class RecordingActivity extends Activity implements View.OnClickListener
     private boolean startRecording() {
         mRecorder = new MediaRecorder();
         mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
-        if (Build.VERSION.SDK_INT >= 29) {
-            mRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG);
+        final int outputFormat;
+        if (Config.USE_OPUS_VOICE_MESSAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            outputFormat = MediaRecorder.OutputFormat.OGG;
+            mRecorder.setOutputFormat(outputFormat);
             mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS);
-            mRecorder.setAudioEncodingBitRate(24000);
-            mRecorder.setAudioSamplingRate(48000);
+            mRecorder.setAudioEncodingBitRate(32000);
         } else {
-            mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
+            outputFormat = MediaRecorder.OutputFormat.MPEG_4;
+            mRecorder.setOutputFormat(outputFormat);
             mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
             mRecorder.setAudioEncodingBitRate(96000);
             mRecorder.setAudioSamplingRate(22050);
         }
-        setupOutputFile();
+        setupOutputFile(outputFormat);
         mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
 
         try {
@@ -117,10 +119,10 @@ public class RecordingActivity extends Activity implements View.OnClickListener
             mRecorder.start();
             mStartTime = SystemClock.elapsedRealtime();
             mHandler.postDelayed(mTickExecutor, 100);
-            Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
+            Log.d(Config.LOGTAG, "started recording to " + mOutputFile.getAbsolutePath());
             return true;
         } catch (Exception e) {
-            Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
+            Log.e(Config.LOGTAG, "prepare() failed ", e);
             return false;
         }
     }
@@ -182,14 +184,18 @@ public class RecordingActivity extends Activity implements View.OnClickListener
         }
     }
 
-    private File generateOutputFilename() {
+    private File generateOutputFilename(final int outputFormat) {
         final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
-        final String filename;
-        if (Build.VERSION.SDK_INT >= 29) {
-            filename = "RECORDING_" + dateFormat.format(new Date()) + ".opus";
+        final String extension;
+        if (outputFormat == MediaRecorder.OutputFormat.MPEG_4) {
+            extension = "m4a";
+        } else if (outputFormat == MediaRecorder.OutputFormat.OGG) {
+            extension = "oga";
         } else {
-            filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
+            throw new IllegalStateException("Unrecognized output format");
         }
+        final String filename =
+                String.format("RECORDING_%s.%s", dateFormat.format(new Date()), extension);
         final File parentDirectory;
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
             parentDirectory =
@@ -202,8 +208,8 @@ public class RecordingActivity extends Activity implements View.OnClickListener
         return new File(conversationsDirectory, filename);
     }
 
-    private void setupOutputFile() {
-        mOutputFile = generateOutputFilename();
+    private void setupOutputFile(final int outputFormat) {
+        mOutputFile = generateOutputFilename(outputFormat);
         final File parentDirectory = mOutputFile.getParentFile();
         if (Objects.requireNonNull(parentDirectory).mkdirs()) {
             Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());

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

@@ -173,6 +173,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
         changeOmemoSettingSummary();
 
         if (QuickConversationsService.isQuicksy()
+                || QuickConversationsService.isPlayStoreFlavor()
                 || Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
             final PreferenceCategory groupChats =
                     (PreferenceCategory) mSettingsFragment.findPreference("group_chats");

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

@@ -61,7 +61,7 @@ public class ShortcutActivity extends AbstractSearchableListItemActivity {
             return;
         }
         for (final Account account : xmppConnectionService.getAccounts()) {
-            if (account.getStatus() != Account.State.DISABLED) {
+            if (account.isEnabled()) {
                 for (final Contact contact : account.getRoster().getContacts()) {
                     if (contact.showInContactList()
                             && contact.match(this, needle)) {

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

@@ -345,7 +345,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             }
             switch (actionItem.getId()) {
                 case R.id.discover_public_channels:
-                    startActivity(new Intent(this, ChannelDiscoveryActivity.class));
+                    if (QuickConversationsService.isPlayStoreFlavor()) {
+                        throw new IllegalStateException("Channel discovery is not available on Google Play flavor");
+                    } else {
+                        startActivity(new Intent(this, ChannelDiscoveryActivity.class));
+                    }
                     break;
                 case R.id.create_private_group_chat:
                     showCreatePrivateGroupChatDialog();
@@ -368,6 +372,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         final Menu menu = popupMenu.getMenu();
         for (int i = 0; i < menu.size(); i++) {
             final MenuItem menuItem = menu.getItem(i);
+            if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) {
+                continue;
+            }
             final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
                     .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
                     .setFabImageTintColor(ContextCompat.getColor(this, R.color.white))
@@ -881,6 +888,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
     @Override
     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         if (grantResults.length > 0)
             if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                 ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
@@ -1091,8 +1099,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         ArrayList<ListItem.Tag> tags = new ArrayList<>();
         final List<Account> accounts = xmppConnectionService.getAccounts();
         boolean foundSopranica = false;
-        for (Account account : accounts) {
-            if (account.getStatus() != Account.State.DISABLED) {
+        for (final Account account : accounts) {
+            if (account.isEnabled()) {
                 for (Contact contact : account.getRoster().getContacts()) {
                     Presence.Status s = contact.getShownStatus();
                     if (contact.showInContactList() && contact.match(this, needle)
@@ -1152,7 +1160,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     protected void filterConferences(String needle) {
         this.conferences.clear();
         for (final Account account : xmppConnectionService.getAccounts()) {
-            if (account.getStatus() != Account.State.DISABLED) {
+            if (account.isEnabled()) {
                 for (final Bookmark bookmark : account.getBookmarks()) {
                     if (bookmark.match(this, needle)) {
                         this.conferences.add(bookmark);

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

@@ -38,12 +38,20 @@ import eu.siacs.conversations.utils.ProvisioningUtils;
 import eu.siacs.conversations.utils.SignupUtils;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xmpp.Jid;
+
 import okhttp3.Call;
 import okhttp3.Callback;
 import okhttp3.HttpUrl;
 import okhttp3.Request;
 import okhttp3.Response;
 
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 public class UriHandlerActivity extends AppCompatActivity {
 
     public static final String ACTION_SCAN_QR_CODE = "scan_qr_code";
@@ -62,7 +70,9 @@ public class UriHandlerActivity extends AppCompatActivity {
     }
 
     public static void scan(final Activity activity, final boolean provisioning) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+                || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
+                        == PackageManager.PERMISSION_GRANTED) {
             final Intent intent = new Intent(activity, UriHandlerActivity.class);
             intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
             if (provisioning) {
@@ -72,14 +82,17 @@ public class UriHandlerActivity extends AppCompatActivity {
             activity.startActivity(intent);
         } else {
             activity.requestPermissions(
-                    new String[]{Manifest.permission.CAMERA},
-                    provisioning ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION : REQUEST_CAMERA_PERMISSIONS_TO_SCAN
-            );
+                    new String[] {Manifest.permission.CAMERA},
+                    provisioning
+                            ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION
+                            : REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
         }
     }
 
-    public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) {
-        if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
+    public static void onRequestPermissionResult(
+            Activity activity, int requestCode, int[] grantResults) {
+        if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN
+                && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
             return;
         }
         if (grantResults.length > 0) {
@@ -90,7 +103,11 @@ public class UriHandlerActivity extends AppCompatActivity {
                     scan(activity);
                 }
             } else {
-                Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show();
+                Toast.makeText(
+                                activity,
+                                R.string.qr_code_scanner_needs_access_to_camera,
+                                Toast.LENGTH_SHORT)
+                        .show();
             }
         }
     }
@@ -141,7 +158,7 @@ public class UriHandlerActivity extends AppCompatActivity {
     private boolean handleUri(final Uri uri, final boolean scanned) {
         final Intent intent;
         final XmppUri xmppUri = new XmppUri(uri);
-        final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
+        final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(false);
 
         if (uri.getScheme().equals("sgnl")) {
             stickers = Uri.parse("https://stickers.cheogram.com/signal/" + uri.getQueryParameter("pack_id") + "," + uri.getQueryParameter("pack_key"));
@@ -170,7 +187,12 @@ public class UriHandlerActivity extends AppCompatActivity {
                 startActivity(intent);
                 return true;
             }
-            if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
+            if (accounts.size() == 0
+                    && xmppUri.isAction(XmppUri.ACTION_ROSTER)
+                    && "y"
+                            .equalsIgnoreCase(
+                                    Strings.nullToEmpty(xmppUri.getParameter(XmppUri.PARAMETER_IBR))
+                                            .trim())) {
                 intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
                 intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
                 startActivity(intent);
@@ -236,29 +258,28 @@ public class UriHandlerActivity extends AppCompatActivity {
 
     private void checkForLinkHeader(final HttpUrl url) {
         Log.d(Config.LOGTAG, "checking for link header on " + url);
-        this.call = HttpConnectionManager.OK_HTTP_CLIENT.newCall(new Request.Builder()
-                .url(url)
-                .head()
-                .build());
-        this.call.enqueue(new Callback() {
-            @Override
-            public void onFailure(@NotNull Call call, @NotNull IOException e) {
-                Log.d(Config.LOGTAG, "unable to check HTTP url", e);
-                showError(R.string.no_xmpp_adddress_found);
-            }
-
-            @Override
-            public void onResponse(@NotNull Call call, @NotNull Response response) {
-                if (response.isSuccessful()) {
-                    final String linkHeader = response.header("Link");
-                    if (linkHeader != null && processLinkHeader(linkHeader)) {
-                        return;
+        this.call =
+                HttpConnectionManager.OK_HTTP_CLIENT.newCall(
+                        new Request.Builder().url(url).head().build());
+        this.call.enqueue(
+                new Callback() {
+                    @Override
+                    public void onFailure(@NotNull Call call, @NotNull IOException e) {
+                        Log.d(Config.LOGTAG, "unable to check HTTP url", e);
+                        showError(R.string.no_xmpp_adddress_found);
                     }
-                }
-                showError(R.string.no_xmpp_adddress_found);
-            }
-        });
 
+                    @Override
+                    public void onResponse(@NotNull Call call, @NotNull Response response) {
+                        if (response.isSuccessful()) {
+                            final String linkHeader = response.header("Link");
+                            if (linkHeader != null && processLinkHeader(linkHeader)) {
+                                return;
+                            }
+                        }
+                        showError(R.string.no_xmpp_adddress_found);
+                    }
+                });
     }
 
     private boolean processLinkHeader(final String header) {
@@ -295,7 +316,8 @@ public class UriHandlerActivity extends AppCompatActivity {
         }
         switch (action) {
             case Intent.ACTION_MAIN:
-                binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
+                binding.progress.setVisibility(
+                        call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
                 break;
             case Intent.ACTION_VIEW:
             case Intent.ACTION_SENDTO:
@@ -319,7 +341,8 @@ public class UriHandlerActivity extends AppCompatActivity {
 
     private boolean allowProvisioning() {
         final Intent launchIntent = getIntent();
-        return launchIntent != null && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false);
+        return launchIntent != null
+                && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false);
     }
 
     @Override
@@ -342,13 +365,17 @@ public class UriHandlerActivity extends AppCompatActivity {
                     showError(R.string.no_xmpp_adddress_found);
                 }
                 return;
-            } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) {
+            } else if (QuickConversationsService.isConversations()
+                    && looksLikeJsonObject(result)
+                    && allowProvisioning) {
                 ProvisioningUtils.provision(this, result);
                 finish();
                 return;
             }
             final Uri uri = Uri.parse(result.trim());
-            if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
+            if (allowProvisioning
+                    && "https".equalsIgnoreCase(uri.getScheme())
+                    && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
                 final HttpUrl httpUrl = HttpUrl.parse(uri.toString());
                 if (httpUrl != null) {
                     checkForLinkHeader(httpUrl);

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

@@ -53,7 +53,6 @@ import android.widget.Toast;
 
 import androidx.annotation.BoolRes;
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AlertDialog.Builder;
@@ -87,17 +86,23 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.ui.util.PresenceSelector;
+import eu.siacs.conversations.ui.util.SettingsUtils;
 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.ExceptionHelper;
-import eu.siacs.conversations.ui.util.SettingsUtils;
 import eu.siacs.conversations.utils.SignupUtils;
 import eu.siacs.conversations.utils.ThemeHelper;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
 public abstract class XmppActivity extends ActionBarActivity {
 
     public static final String EXTRA_ACCOUNT = "account";
@@ -327,22 +332,24 @@ public abstract class XmppActivity extends ActionBarActivity {
                         button.setText(R.string.please_wait);
                         button.setEnabled(false);
                         xmppConnectionService.unregisterAccount(account, result -> {
-                            if (result) {
-                                dialog.dismiss();
-                                if (postDelete != null) {
-                                    postDelete.run();
-                                }
-                                if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
-                                    final Intent intent = SignupUtils.getSignUpIntent(this);
-                                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-                                    startActivity(intent);
+                            runOnUiThread(()->{
+                                if (result) {
+                                    dialog.dismiss();
+                                    if (postDelete != null) {
+                                        postDelete.run();
+                                    }
+                                    if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+                                        final Intent intent = SignupUtils.getSignUpIntent(this);
+                                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                                        startActivity(intent);
+                                    }
+                                } else {
+                                    deleteFromServer.setEnabled(true);
+                                    button.setText(R.string.delete);
+                                    button.setEnabled(true);
+                                    Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show();
                                 }
-                            } else {
-                                deleteFromServer.setEnabled(true);
-                                button.setText(R.string.delete);
-                                button.setEnabled(true);
-                                Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show();
-                            }
+                            });
                         });
                     } else {
                         Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_LONG).show();
@@ -671,9 +678,9 @@ public abstract class XmppActivity extends ActionBarActivity {
             xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
 
                 @Override
-                public void userInputRequired(PendingIntent pi, String signature) {
+                public void userInputRequired(final PendingIntent pi, final String signature) {
                     try {
-                        startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+                        startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0,Compatibility.pgpStartIntentSenderOptions());
                     } catch (final SendIntentException ignored) {
                     }
                 }
@@ -734,7 +741,7 @@ public abstract class XmppActivity extends ActionBarActivity {
             public void userInputRequired(PendingIntent pi, Account object) {
                 try {
                     startIntentSenderForResult(pi.getIntentSender(),
-                            REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
+                            REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
                 } catch (final SendIntentException ignored) {
                 }
             }
@@ -938,8 +945,9 @@ public abstract class XmppActivity extends ActionBarActivity {
         try {
             startIntentSenderForResult(
                     pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0,
-                    0, 0);
-        } catch (Throwable e) {
+                    0, 0, Compatibility.pgpStartIntentSenderOptions());
+        } catch (final Throwable e) {
+            Log.d(Config.LOGTAG,"could not launch OpenKeyChain", e);
             Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
         }
     }

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

@@ -61,6 +61,7 @@ public class AccountAdapter extends ArrayAdapter<Account> {
                 viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
                 break;
             case DISABLED:
+            case LOGGED_OUT:
             case CONNECTING:
                 viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
                 break;

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

@@ -74,6 +74,8 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
             Log.d(Config.LOGTAG, "mime=" + mime);
             if (mime == null) {
                 attr = R.attr.media_preview_unknown;
+            } else if (mime.equals("audio/x-m4b")) {
+                attr = R.attr.media_preview_audiobook;
             } else if (mime.startsWith("audio/")) {
                 attr = R.attr.media_preview_audio;
             } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) {

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

@@ -151,7 +151,7 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
         this.mediaPreviews.clear();
     }
 
-    class MediaPreviewViewHolder extends RecyclerView.ViewHolder {
+    static class MediaPreviewViewHolder extends RecyclerView.ViewHolder {
 
         private final MediaPreviewBinding binding;
 

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

@@ -30,6 +30,7 @@ import eu.siacs.conversations.ui.ConferenceDetailsActivity;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.xmpp.Jid;
 
 public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHolder> implements View.OnCreateContextMenuListener {
@@ -109,7 +110,7 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
                     PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId());
                     if (intent != null) {
                         try {
-                            activity.startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0);
+                            activity.startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
                         } catch (IntentSender.SendIntentException ignored) {
 
                         }

src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java 🔗

@@ -9,7 +9,6 @@ import android.view.View;
 
 import java.util.List;
 
-import eu.siacs.conversations.R;
 import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.xmpp.forms.Field;
 
@@ -58,7 +57,7 @@ public abstract class FormFieldWrapper {
 			int start = label.length();
 			int end = label.length() + 2;
 			spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0);
-			spannableString.setSpan(new ForegroundColorSpan(StyledAttributes.getColor(context,R.attr.colorAccent)), start, end, 0);
+			spannableString.setSpan(new ForegroundColorSpan(StyledAttributes.getColor(context, androidx.appcompat.R.attr.colorAccent)), start, end, 0);
 		}
 		return spannableString;
 	}

src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java 🔗

@@ -26,7 +26,6 @@ import android.widget.ListView;
 import androidx.fragment.app.ListFragment;
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 
-import eu.siacs.conversations.R;
 import eu.siacs.conversations.ui.util.StyledAttributes;
 
 /**
@@ -57,7 +56,7 @@ public class SwipeRefreshListFragment extends ListFragment {
 
         final Context context = getActivity();
         if (context != null) {
-            mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, R.attr.colorAccent));
+            mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, androidx.appcompat.R.attr.colorAccent));
         }
 
         if (onRefreshListener != null) {

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

@@ -60,7 +60,7 @@ public class AccountUtils {
     public static List<String> getEnabledAccounts(final XmppConnectionService service) {
         final ArrayList<String> accounts = new ArrayList<>();
         for (final Account account : service.getAccounts()) {
-            if (account.getStatus() != Account.State.DISABLED) {
+            if (account.isEnabled()) {
                 if (Config.DOMAIN_LOCK != null) {
                     accounts.add(account.getJid().getEscapedLocal());
                 } else {

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

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.utils;
 
+import androidx.annotation.NonNull;
+
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
@@ -8,7 +10,7 @@ import eu.siacs.conversations.xmpp.Jid;
 
 public class BackupFileHeader {
 
-    private static final int VERSION = 1;
+    private static final int VERSION = 2;
 
     private final String app;
     private final Jid jid;
@@ -17,6 +19,7 @@ public class BackupFileHeader {
     private final byte[] salt;
 
 
+    @NonNull
     @Override
     public String toString() {
         return "BackupFileHeader{" +
@@ -47,17 +50,19 @@ public class BackupFileHeader {
 
     public static BackupFileHeader read(DataInputStream inputStream) throws IOException {
         final int version = inputStream.readInt();
-        if (version > VERSION) {
-            throw new IllegalArgumentException("Backup File version was " + version + " but app only supports up to version " + VERSION);
-        }
-        String app = inputStream.readUTF();
-        String jid = inputStream.readUTF();
+        final String app = inputStream.readUTF();
+        final String jid = inputStream.readUTF();
         long timestamp = inputStream.readLong();
-        byte[] iv = new byte[12];
+        final byte[] iv = new byte[12];
         inputStream.readFully(iv);
-        byte[] salt = new byte[16];
+        final byte[] salt = new byte[16];
         inputStream.readFully(salt);
-
+        if (version < VERSION) {
+            throw new OutdatedBackupFileVersion();
+        }
+        if (version != VERSION) {
+            throw new IllegalArgumentException("Backup File version was " + version + " but app only supports version " + VERSION);
+        }
         return new BackupFileHeader(app, Jid.of(jid), timestamp, iv, salt);
 
     }
@@ -81,4 +86,8 @@ public class BackupFileHeader {
     public long getTimestamp() {
         return timestamp;
     }
+
+    public static class OutdatedBackupFileVersion extends RuntimeException {
+
+    }
 }

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

@@ -3,6 +3,7 @@ package eu.siacs.conversations.utils;
 import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
 
 import android.annotation.SuppressLint;
+import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -10,6 +11,7 @@ import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.os.Build;
+import android.os.Bundle;
 import android.preference.Preference;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceManager;
@@ -20,15 +22,15 @@ import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.core.content.ContextCompat;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.SettingsFragment;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
 public class Compatibility {
 
     private static final List<String> UNUSED_SETTINGS_POST_TWENTYSIX =
@@ -41,7 +43,8 @@ public class Compatibility {
             Collections.singletonList("message_notification_settings");
 
     public static boolean hasStoragePermission(final Context context) {
-        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+                || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
                 || ContextCompat.checkSelfPermission(
                                 context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
                         == PackageManager.PERMISSION_GRANTED;
@@ -189,4 +192,15 @@ public class Compatibility {
             return ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
         }
     }
+
+    public static Bundle pgpStartIntentSenderOptions() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            return ActivityOptions.makeBasic()
+                    .setPendingIntentBackgroundActivityStartMode(
+                            ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+                    .toBundle();
+        } else {
+            return null;
+        }
+    }
 }

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

@@ -34,10 +34,8 @@ public class FileUtils {
 			return null;
 		}
 
-		final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
-
 		// DocumentProvider
-		if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+		if (DocumentsContract.isDocumentUri(context, uri)) {
 			// ExternalStorageProvider
 			if (isExternalStorageDocument(uri)) {
 				final String docId = DocumentsContract.getDocumentId(uri);

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

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.utils;
 
+import com.google.common.net.InetAddresses;
+
 import java.util.regex.Pattern;
 
 public class IP {
@@ -27,4 +29,14 @@ public class IP {
         }
     }
 
+    public static String unwrapIPv6(final String host) {
+        if (host.length() > 2 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') {
+            final String ip = host.substring(1,host.length() -1);
+            if (InetAddresses.isInetAddress(ip)) {
+                return ip;
+            }
+        }
+        return host;
+    }
+
 }

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

@@ -60,7 +60,7 @@ public class IrregularUnicodeDetector {
 
 	private static final Map<Character.UnicodeBlock, Character.UnicodeBlock> NORMALIZATION_MAP;
 	private static final LruCache<Jid, PatternTuple> CACHE = new LruCache<>(4096);
-	private static final List<String> AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","ј","ԛ","о","р","с","у","х");
+	private static final List<String> AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","ј","ķ","ԛ","о","р","с","у","х");
 
 	static {
 		Map<Character.UnicodeBlock, Character.UnicodeBlock> temp = new HashMap<>();

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

@@ -28,6 +28,8 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -262,6 +264,7 @@ public final class MimeUtils {
         add("audio/mpeg", "mpega");
         add("audio/mpeg", "mp2");
         add("audio/mp4", "m4a");
+        add("audio/x-m4b", "m4b");
         add("audio/mpegurl", "m3u");
         add("audio/ogg", "oga");
         add("audio/ogg; codecs=opus", "opus"); // opus in ogg container
@@ -416,6 +419,9 @@ public final class MimeUtils {
         applyOverrides();
     }
 
+    // mime types that are more reliant by path
+    private static final Collection<String> PATH_PRECEDENCE_MIME_TYPE = Arrays.asList("audio/x-m4b");
+
     private static void add(String mimeType, String extension) {
         // If we have an existing x -> y mapping, we do not want to
         // override it with another mapping x -> y2.
@@ -540,43 +546,49 @@ public final class MimeUtils {
     }
 
     public static String guessMimeTypeFromUriAndMime(final Context context, final Uri uri, final String mime) {
-        Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime " + uri + " and mime=" + mime);
-        final String guess = guessMimeTypeFromUri(context, uri);
-        if (guess != null) {
-            return guess;
+        Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime(" + uri + "," + mime+")");
+        final String mimeFromUri = guessMimeTypeFromUri(context, uri);
+        Log.d(Config.LOGTAG,"mimeFromUri:"+mimeFromUri);
+        if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeFromUri)) {
+            return mimeFromUri;
+        } else if (mime == null || mime.equals("application/octet-stream")) {
+            return mimeFromUri;
         } else {
             return mime;
         }
     }
 
-    public static String guessMimeTypeFromUri(Context context, Uri uri) {
-        // try the content resolver
-        String mimeType;
-        try {
-            mimeType = context.getContentResolver().getType(uri);
-        } catch (final Throwable throwable) {
-            mimeType = null;
+    public static String guessMimeTypeFromUri(final Context context, final Uri uri) {
+        final String mimeTypeContentResolver = guessFromContentResolver(context, uri);
+        final String mimeTypeFromQueryParameter = uri.getQueryParameter("mimeType");
+        final String name = "content".equals(uri.getScheme()) ? getDisplayName(context, uri) : null;
+        final String mimeTypeFromName = Strings.isNullOrEmpty(name) ? null : guessFromPath(name);
+        final String path = uri.getPath();
+        final String mimeTypeFromPath = Strings.isNullOrEmpty(path) ? null : guessFromPath(path);
+        if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromName)) {
+            return mimeTypeFromName;
         }
-        // try the extension
-        if (mimeType == null || mimeType.equals("application/octet-stream")) {
-            final String path = uri.getPath();
-            if (path != null) {
-                mimeType = guessFromPath(path);
-            }
+        if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromPath)) {
+            return mimeTypeFromPath;
         }
-        if (mimeType == null && "content".equals(uri.getScheme())) {
-            final String name = getDisplayName(context, uri);
-            if (name != null) {
-                mimeType = guessFromPath(name);
-            }
+        if (mimeTypeContentResolver != null && !"application/octet-stream".equals(mimeTypeContentResolver)) {
+            return mimeTypeContentResolver;
         }
-        // sometimes this works (as with the commit content api)
-        if (mimeType == null) {
-            try {
-                mimeType = uri.getQueryParameter("mimeType");
-            } catch (final Throwable throwable) { }
+        if (mimeTypeFromName != null) {
+            return mimeTypeFromName;
+        }
+        if (mimeTypeFromQueryParameter != null) {
+            return mimeTypeFromQueryParameter;
+        }
+        return mimeTypeFromPath;
+    }
+
+    private static String guessFromContentResolver(final Context context, final Uri uri) {
+        try {
+            return context.getContentResolver().getType(uri);
+        } catch (final Throwable e) {
+            return null;
         }
-        return mimeType;
     }
 
     private static String getDisplayName(final Context context, final Uri uri) {

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

@@ -24,9 +24,23 @@ public class PermissionUtils {
         return true;
     }
 
-    public static boolean writeGranted(int[] grantResults, String[] permission) {
+    public static boolean writeGranted(final int[] grantResults, final String[] permissions) {
+        return permissionGranted(
+                Manifest.permission.WRITE_EXTERNAL_STORAGE, grantResults, permissions);
+    }
+
+    public static boolean audioGranted(final int[] grantResults, final String[] permissions) {
+        return permissionGranted(Manifest.permission.RECORD_AUDIO, grantResults, permissions);
+    }
+
+    public static boolean cameraGranted(final int[] grantResults, final String[] permissions) {
+        return permissionGranted(Manifest.permission.CAMERA, grantResults, permissions);
+    }
+
+    private static boolean permissionGranted(
+            final String permission, final int[] grantResults, final String[] permissions) {
         for (int i = 0; i < grantResults.length; ++i) {
-            if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
+            if (permission.equals(permissions[i])) {
                 return grantResults[i] == PackageManager.PERMISSION_GRANTED;
             }
         }
@@ -72,7 +86,7 @@ public class PermissionUtils {
 
     public static boolean hasPermission(
             final Activity activity, final List<String> permissions, final int requestCode) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             final ImmutableList.Builder<String> missingPermissions = new ImmutableList.Builder<>();
             for (final String permission : permissions) {
                 if (ActivityCompat.checkSelfPermission(activity, permission)

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

@@ -6,6 +6,11 @@ import android.util.Log;
 
 import androidx.annotation.NonNull;
 
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.net.InetAddresses;
+import com.google.common.primitives.Ints;
+
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.net.Inet4Address;
@@ -15,6 +20,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import de.gultsch.minidns.AndroidDNSClient;
 import de.measite.minidns.AbstractDNSClient;
 import de.measite.minidns.DNSCache;
 import de.measite.minidns.DNSClient;
@@ -112,7 +118,7 @@ public class Resolver {
         return port == 443 || port == 5223;
     }
 
-    public static List<Result> resolve(String domain) {
+    public static List<Result> resolve(final String domain) {
         final  List<Result> ipResults = fromIpAddress(domain);
         if (ipResults.size() > 0) {
             return ipResults;
@@ -126,8 +132,10 @@ public class Resolver {
                 synchronized (results) {
                     results.addAll(list);
                 }
-            } catch (Throwable throwable) {
-                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
+            } catch (final Throwable throwable) {
+                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
+                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
+                }
             }
         });
         threads[1] = new Thread(() -> {
@@ -136,8 +144,10 @@ public class Resolver {
                 synchronized (results) {
                     results.addAll(list);
                 }
-            } catch (Throwable throwable) {
-                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
+            } catch (final Throwable throwable) {
+                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
+                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
+                }
             }
         });
         threads[2] = new Thread(() -> {
@@ -260,8 +270,10 @@ public class Resolver {
                     results.addAll(resolveNoSrvRecords(cname.name, false));
                 }
             }
-        } catch (Throwable throwable) {
-            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
+        } catch (final Throwable throwable) {
+            if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
+                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
+            }
         }
         results.add(Result.createDefault(dnsName));
         return results;
@@ -274,7 +286,9 @@ public class Resolver {
     private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type, boolean validateHostname) throws IOException {
         final Question question = new Question(dnsName, Record.TYPE.getType(type));
         if (!validateHostname) {
-            return ResolverApi.INSTANCE.resolve(question);
+            final AndroidDNSClient androidDNSClient = new AndroidDNSClient(SERVICE);
+            final ResolverApi resolverApi = new ResolverApi(androidDNSClient);
+            return resolverApi.resolve(question);
         }
         try {
             return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question);
@@ -435,6 +449,65 @@ public class Resolver {
             contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
             return contentValues;
         }
+
+        public Result seeOtherHost(final String seeOtherHost) {
+            final String hostname = seeOtherHost.trim();
+            if (hostname.isEmpty()) {
+                return null;
+            }
+            final Result result = new Result();
+            result.directTls = this.directTls;
+            final int portSegmentStart = hostname.lastIndexOf(':');
+            if (hostname.charAt(hostname.length() - 1) != ']'
+                    && portSegmentStart >= 0
+                    && hostname.length() >= portSegmentStart + 1) {
+                final String hostPart = hostname.substring(0, portSegmentStart);
+                final String portPart = hostname.substring(portSegmentStart + 1);
+                final Integer port = Ints.tryParse(portPart);
+                if (port == null || Strings.isNullOrEmpty(hostPart)) {
+                    return null;
+                }
+                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
+                result.port = port;
+                if (InetAddresses.isInetAddress(host)) {
+                    final InetAddress inetAddress;
+                    try {
+                        inetAddress = InetAddresses.forString(host);
+                    } catch (final IllegalArgumentException e) {
+                        return null;
+                    }
+                    result.ip = inetAddress;
+                } else {
+                    if (hostPart.trim().isEmpty()) {
+                        return null;
+                    }
+                    try {
+                        result.hostname = DNSName.from(hostPart.trim());
+                    } catch (final Exception e) {
+                        return null;
+                    }
+                }
+            } else {
+                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
+                if (InetAddresses.isInetAddress(host)) {
+                    final InetAddress inetAddress;
+                    try {
+                        inetAddress = InetAddresses.forString(host);
+                    } catch (final IllegalArgumentException e) {
+                        return null;
+                    }
+                    result.ip = inetAddress;
+                } else {
+                    try {
+                        result.hostname = DNSName.from(hostname);
+                    } catch (final Exception e) {
+                        return null;
+                    }
+                }
+                result.port = port;
+            }
+            return result;
+        }
     }
 
 }

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

@@ -498,6 +498,8 @@ public class UIHelper {
             return context.getString(R.string.file);
         } else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) {
             return context.getString(R.string.multimedia_file);
+        } else if (mime.equals("audio/x-m4b")) {
+            return context.getString(R.string.audiobook);
         } else if (mime.startsWith("audio/")) {
             return context.getString(R.string.audio);
         } else if (mime.startsWith("video/")) {

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -68,6 +68,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.XmppDomainVerifier;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.sasl.ChannelBinding;
+import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
 import eu.siacs.conversations.crypto.sasl.HashedToken;
 import eu.siacs.conversations.crypto.sasl.SaslMechanism;
 import eu.siacs.conversations.entities.Account;
@@ -192,6 +193,8 @@ public class XmppConnection implements Runnable {
     private HashedToken.Mechanism hashTokenRequest;
     private HttpUrl redirectionUrl = null;
     private String verifiedHostname = null;
+    private Resolver.Result currentResolverResult;
+    private Resolver.Result seeOtherHostResolverResult;
     private volatile Thread mThread;
     private CountDownLatch mStreamCountDownLatch;
     private static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
@@ -234,10 +237,11 @@ public class XmppConnection implements Runnable {
                 return;
             }
             if (account.getStatus() != nextStatus) {
-                if ((nextStatus == Account.State.OFFLINE)
-                        && (account.getStatus() != Account.State.CONNECTING)
-                        && (account.getStatus() != Account.State.ONLINE)
-                        && (account.getStatus() != Account.State.DISABLED)) {
+                if (nextStatus == Account.State.OFFLINE
+                        && account.getStatus() != Account.State.CONNECTING
+                        && account.getStatus() != Account.State.ONLINE
+                        && account.getStatus() != Account.State.DISABLED
+                        && account.getStatus() != Account.State.LOGGED_OUT) {
                     return;
                 }
                 if (nextStatus == Account.State.ONLINE) {
@@ -364,7 +368,12 @@ public class XmppConnection implements Runnable {
                                         + storedBackupResult);
                     }
                 }
-                for (Iterator<Resolver.Result> iterator = results.iterator();
+                final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
+                if (seeOtherHost != null) {
+                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected see-other-host on position 0");
+                    results.add(0, seeOtherHost);
+                }
+                for (final Iterator<Resolver.Result> iterator = results.iterator();
                         iterator.hasNext(); ) {
                     final Resolver.Result result = iterator.next();
                     if (Thread.currentThread().isInterrupted()) {
@@ -378,7 +387,6 @@ public class XmppConnection implements Runnable {
                         features.encryptionEnabled = result.isDirectTls();
                         verifiedHostname =
                                 result.isAuthenticated() ? result.getHostname().toString() : null;
-                        Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname);
                         final InetSocketAddress addr;
                         if (result.getIp() != null) {
                             addr = new InetSocketAddress(result.getIp(), result.getPort());
@@ -426,6 +434,8 @@ public class XmppConnection implements Runnable {
                                 mXmppConnectionService.databaseBackend.saveResolverResult(
                                         domain, result);
                             }
+                            this.currentResolverResult = result;
+                            this.seeOtherHostResolverResult = null;
                             break; // successfully connected to server that speaks xmpp
                         } else {
                             FileBackend.close(localSocket);
@@ -822,10 +832,15 @@ public class XmppConnection implements Runnable {
                 tokenMechanism = null;
             }
             if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
-                this.account.setFastToken(tokenMechanism, token);
-                Log.d(
-                        Config.LOGTAG,
-                        account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism);
+                if (ChannelBinding.priority(tokenMechanism.channelBinding) >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
+                    this.account.setFastToken(tokenMechanism, token);
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism);
+                } else {
+                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": not accepting hashed token "+ tokenMechanism.name()+" for log in mechanism "+currentSaslMechanism.getMechanism());
+                    this.account.resetFastToken();
+                }
             } else if (this.hashTokenRequest != null) {
                 Log.w(
                         Config.LOGTAG,
@@ -1255,8 +1270,9 @@ public class XmppConnection implements Runnable {
         tagReader.readTag();
         final Socket socket = this.socket;
         final SSLSocket sslSocket = upgradeSocketToTls(socket);
-        tagReader.setInputStream(sslSocket.getInputStream());
-        tagWriter.setOutputStream(sslSocket.getOutputStream());
+        this.socket = sslSocket;
+        this.tagReader.setInputStream(sslSocket.getInputStream());
+        this.tagWriter.setOutputStream(sslSocket.getOutputStream());
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
         final boolean quickStart;
         try {
@@ -2177,6 +2193,21 @@ public class XmppConnection implements Runnable {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
             failPendingMessages(text);
             throw new StateChangingException(Account.State.POLICY_VIOLATION);
+        } else if (streamError.hasChild("see-other-host")) {
+            final String seeOtherHost = streamError.findChildContent("see-other-host");
+            final Resolver.Result currentResolverResult = this.currentResolverResult;
+            if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
+                throw new StateChangingException(Account.State.STREAM_ERROR);
+            }
+            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult);
+            final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
+            if (seeOtherResult != null) {
+                this.seeOtherHostResolverResult = seeOtherResult;
+                throw new StateChangingException(Account.State.SEE_OTHER_HOST);
+            } else {
+                throw new StateChangingException(Account.State.STREAM_ERROR);
+            }
         } else {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
             throw new StateChangingException(Account.State.STREAM_ERROR);
@@ -2200,9 +2231,13 @@ public class XmppConnection implements Runnable {
 
     private boolean establishStream(final SSLSockets.Version sslVersion)
             throws IOException, InterruptedException {
-        final SaslMechanism quickStartMechanism =
-                SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
         final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
+        final SaslMechanism quickStartMechanism;
+        if (secureConnection) {
+            quickStartMechanism = SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
+        } else {
+            quickStartMechanism = null;
+        }
         if (secureConnection
                 && Config.QUICKSTART_ENABLED
                 && quickStartMechanism != null

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

@@ -73,7 +73,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     static String nextRandomId() {
         final byte[] id = new byte[16];
         new SecureRandom().nextBytes(id);
-        return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING);
+        return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE);
     }
 
     public void deliverPacket(final Account account, final JinglePacket packet) {
@@ -100,7 +100,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                         this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id));
                 final boolean stranger =
                         isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
-                if (isBusy() != null || sessionEnded || stranger) {
+                final boolean busy = isBusy() != null;
+                if (busy || sessionEnded || stranger) {
                     Log.d(
                             Config.LOGTAG,
                             id.account.getJid().asBareJid()
@@ -117,6 +118,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     sessionTermination.setTo(id.with);
                     sessionTermination.setReason(Reason.BUSY, null);
                     mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
+                    if (busy || stranger) {
+                        writeLogMissedIncoming(
+                                account,
+                                id.with,
+                                id.sessionId,
+                                null,
+                                System.currentTimeMillis(),
+                                stranger);
+                    }
                     return;
                 }
                 connection = new JingleRtpConnection(this, id, from);
@@ -283,6 +293,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             if ("accept".equals(message.getName())) return;
         }
         final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid());
+        // XEP version 0.6.0 sends proceed, reject, ringing to bare jid
         final boolean addressedDirectly = to != null && to.equals(account.getJid());
         final AbstractJingleConnection.Id id;
         if (fromSelf) {
@@ -327,6 +338,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                             Config.LOGTAG,
                             id.account.getJid().asBareJid()
                                     + ": updated previous busy because call got picked up by another device");
+                    mXmppConnectionService.getNotificationService().clearMissedCall(previousBusy);
                     return;
                 }
             }
@@ -383,18 +395,27 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                         this.connections.put(id, rtpConnection);
                         rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
                         rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
+                        // TODO actually do the automatic accept?!
                     } else {
                         Log.d(
                                 Config.LOGTAG,
                                 account.getJid().asBareJid()
                                         + ": our session won tie break. waiting for other party to accept. winningSession="
                                         + ourSessionId);
+                        // TODO reject their session with <tie-break/>?
                     }
                     return;
                 }
-                final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
+                final boolean stranger =
+                        isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
                 if (isBusy() != null || stranger) {
-                    writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp);
+                    writeLogMissedIncoming(
+                            account,
+                            id.with.asBareJid(),
+                            id.sessionId,
+                            serverMsgId,
+                            timestamp,
+                            stranger);
                     if (stranger) {
                         Log.d(
                                 Config.LOGTAG,
@@ -450,7 +471,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     Log.d(
                             Config.LOGTAG,
                             account.getJid().asBareJid()
-                                    + ": no rtp session proposal found for "
+                                    + ": no rtp session ("
+                                    + sessionId
+                                    + ") proposal found for "
                                     + from
                                     + " to deliver proceed");
                     if (remoteMsgId == null) {
@@ -489,6 +512,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                                     + " to deliver reject");
                 }
             }
+        } else if (addressedDirectly && "ringing".equals(message.getName())) {
+            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + from + " started ringing");
+            updateProposedSessionDiscovered(
+                    account, from, sessionId, DeviceDiscoveryState.DISCOVERED);
         } else {
             Log.d(
                     Config.LOGTAG,
@@ -532,10 +559,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
     private void writeLogMissedIncoming(
             final Account account,
-            Jid with,
+            final Jid with,
             final String sessionId,
-            String serverMsgId,
-            long timestamp) {
+            final String serverMsgId,
+            final long timestamp,
+            final boolean stranger) {
         final Conversation conversation =
                 mXmppConnectionService.findOrCreateConversation(
                         account, with.asBareJid(), false, false);
@@ -545,7 +573,12 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         message.setBody(new RtpSessionStatus(false, 0).toString());
         message.setServerMsgId(serverMsgId);
         message.setTime(timestamp);
+        message.setCounterpart(with);
         writeMessage(message);
+        if (stranger) {
+            return;
+        }
+        mXmppConnectionService.getNotificationService().pushMissedCallNow(message);
     }
 
     private void writeMessage(final Message message) {

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

@@ -409,7 +409,29 @@ public class JingleRtpConnection extends AbstractJingleConnection
             return;
         }
         if (isInState(State.SESSION_ACCEPTED)) {
-            receiveContentAdd(jinglePacket, modification);
+            final boolean hasFullTransportInfo = modification.hasFullTransportInfo();
+            final ListenableFuture<RtpContentMap> future =
+                    receiveRtpContentMap(
+                            modification, this.omemoVerification.hasFingerprint() && hasFullTransportInfo);
+            Futures.addCallback(future, new FutureCallback<RtpContentMap>() {
+                @Override
+                public void onSuccess(final RtpContentMap rtpContentMap) {
+                    receiveContentAdd(jinglePacket, rtpContentMap);
+                }
+
+                @Override
+                public void onFailure(@NonNull Throwable throwable) {
+                    respondOk(jinglePacket);
+                    final Throwable rootCause = Throwables.getRootCause(throwable);
+                    Log.d(
+                            Config.LOGTAG,
+                            id.account.getJid().asBareJid()
+                                    + ": improperly formatted contents in content-add",
+                            throwable);
+                    webRTCWrapper.close();
+                    sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
+                }
+            }, MoreExecutors.directExecutor());
         } else {
             terminateWithOutOfOrder(jinglePacket);
         }
@@ -494,7 +516,22 @@ public class JingleRtpConnection extends AbstractJingleConnection
         if (ourSummary.equals(ContentAddition.summary(receivedContentAccept))) {
             this.outgoingContentAdd = null;
             respondOk(jinglePacket);
-            receiveContentAccept(receivedContentAccept);
+            final boolean hasFullTransportInfo = receivedContentAccept.hasFullTransportInfo();
+            final ListenableFuture<RtpContentMap> future =
+                    receiveRtpContentMap(
+                            receivedContentAccept, this.omemoVerification.hasFingerprint() && hasFullTransportInfo);
+            Futures.addCallback(future, new FutureCallback<RtpContentMap>() {
+                @Override
+                public void onSuccess(final RtpContentMap result) {
+                    receiveContentAccept(result);
+                }
+
+                @Override
+                public void onFailure(@NonNull final Throwable throwable) {
+                    webRTCWrapper.close();
+                    sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage());
+                }
+            }, MoreExecutors.directExecutor());
         } else {
             Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add");
             terminateWithOutOfOrder(jinglePacket);
@@ -527,17 +564,20 @@ public class JingleRtpConnection extends AbstractJingleConnection
             sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
             return;
         }
-        processCandidates(receivedContentAccept.contents.entrySet());
-        updateEndUserState();
         Log.d(
                 Config.LOGTAG,
                 id.getAccount().getJid().asBareJid()
                         + ": remote has accepted content-add "
                         + ContentAddition.summary(receivedContentAccept));
+        processCandidates(receivedContentAccept.contents.entrySet());
+        updateEndUserState();
     }
 
     private void receiveContentModify(final JinglePacket jinglePacket) {
-        // TODO check session accepted
+        if (this.state != State.SESSION_ACCEPTED) {
+            terminateWithOutOfOrder(jinglePacket);
+            return;
+        }
         final Map<String, Content.Senders> modification =
                 Maps.transformEntries(
                         jinglePacket.getJingleContents(), (key, value) -> value.getSenders());
@@ -855,6 +895,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             final RtpContentMap contentAcceptMap =
                     rtpContentMap.toContentModification(
                             Collections2.transform(contentAddition, ca -> ca.name));
+
             Log.d(
                     Config.LOGTAG,
                     id.getAccount().getJid().asBareJid()
@@ -864,8 +905,22 @@ public class JingleRtpConnection extends AbstractJingleConnection
             addIceCandidatesFromBlackLog();
 
             modifyLocalContentMap(rtpContentMap);
-            sendContentAccept(contentAcceptMap);
-            this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
+            final ListenableFuture<RtpContentMap> future = prepareOutgoingContentMap(contentAcceptMap);
+            Futures.addCallback(
+                    future,
+                    new FutureCallback<RtpContentMap>() {
+                        @Override
+                        public void onSuccess(final RtpContentMap rtpContentMap) {
+                            sendContentAccept(rtpContentMap);
+                            webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
+                        }
+
+                        @Override
+                        public void onFailure(@NonNull final Throwable throwable) {
+                            failureToPerformAction(JinglePacket.Action.CONTENT_ACCEPT, throwable);
+                        }
+                    },
+                    MoreExecutors.directExecutor());
         } catch (final Exception e) {
             Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e));
             webRTCWrapper.close();
@@ -1078,12 +1133,20 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private ListenableFuture<RtpContentMap> receiveRtpContentMap(
             final JinglePacket jinglePacket, final boolean expectVerification) {
-        final RtpContentMap receivedContentMap;
         try {
-            receivedContentMap = RtpContentMap.of(jinglePacket);
+            return receiveRtpContentMap(RtpContentMap.of(jinglePacket), expectVerification);
         } catch (final Exception e) {
             return Futures.immediateFailedFuture(e);
         }
+        }
+        private ListenableFuture<RtpContentMap> receiveRtpContentMap(final RtpContentMap receivedContentMap, final boolean expectVerification) {
+        Log.d(
+                Config.LOGTAG,
+                "receiveRtpContentMap("
+                        + receivedContentMap.getClass().getSimpleName()
+                        + ",expectVerification="
+                        + expectVerification
+                        + ")");
         if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) {
             final ListenableFuture<AxolotlService.OmemoVerifiedPayload<RtpContentMap>> future =
                     id.account
@@ -1389,6 +1452,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
         sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
     }
 
+    private void failureToPerformAction(final JinglePacket.Action action, final Throwable throwable) {
+        if (isTerminated()) {
+            return;
+        }
+        final Throwable rootCause = Throwables.getRootCause(throwable);
+        Log.d(Config.LOGTAG, "unable to send " + action, rootCause);
+        webRTCWrapper.close();
+        sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
+    }
+
     private void addIceCandidatesFromBlackLog() {
         Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
         while ((foo = this.pendingIceCandidates.poll()) != null) {
@@ -1663,6 +1736,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
             }
             this.message.setTime(timestamp);
             startRinging();
+            if (xmppConnectionService.confirmMessages() && id.getContact().showInContactList()) {
+                sendJingleMessage("ringing");
+            }
         } else {
             Log.d(
                     Config.LOGTAG,
@@ -1980,7 +2056,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
         final JinglePacket jinglePacket =
                 new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
         jinglePacket.setReason(reason, text);
-        Log.d(Config.LOGTAG, jinglePacket.toString());
         send(jinglePacket);
         finish();
     }
@@ -2558,8 +2633,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
             sessionDescription = setLocalSessionDescription();
         } catch (final Exception e) {
             final Throwable cause = Throwables.getRootCause(e);
-            Log.d(Config.LOGTAG, "failed to renegotiate", cause);
             webRTCWrapper.close();
+            if (isTerminated()) {
+                Log.d(Config.LOGTAG, "failed to renegotiate. session was already terminated", cause);
+                return;
+            }
+            Log.d(Config.LOGTAG, "failed to renegotiate. sending session-terminate", cause);
             sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
             return;
         }
@@ -2637,6 +2716,27 @@ public class JingleRtpConnection extends AbstractJingleConnection
     private void sendContentAdd(final RtpContentMap rtpContentMap, final Collection<String> added) {
         final RtpContentMap contentAdd = rtpContentMap.toContentModification(added);
         this.outgoingContentAdd = contentAdd;
+        final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
+                prepareOutgoingContentMap(contentAdd);
+        Futures.addCallback(
+                outgoingContentMapFuture,
+                new FutureCallback<RtpContentMap>() {
+                    @Override
+                    public void onSuccess(final RtpContentMap outgoingContentMap) {
+                        sendContentAdd(outgoingContentMap);
+                        webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable throwable) {
+                        failureToPerformAction(JinglePacket.Action.CONTENT_ADD, throwable);
+                    }
+                },
+                MoreExecutors.directExecutor());
+    }
+
+    private void sendContentAdd(final RtpContentMap contentAdd) {
+
         final JinglePacket jinglePacket =
                 contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId);
         jinglePacket.setTo(id.with);
@@ -2837,7 +2937,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
                                         // STUN URLs do not support a query section since M110
                                         final String uri;
                                         if (Arrays.asList("stun","stuns").contains(type)) {
-                                            uri = String.format("%s:%s%s", type, IP.wrapIPv6(host),port);
+                                            uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host),port);
                                         } else {
                                             uri = String.format(
                                                     "%s:%s:%s?transport=%s",

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

@@ -275,6 +275,11 @@ public class RtpContentMap {
         return count == 0;
     }
 
+    public boolean hasFullTransportInfo() {
+        return Collections2.transform(this.contents.values(), dt -> dt.transport.isStub())
+                .contains(false);
+    }
+
     public RtpContentMap modifiedCredentials(
             IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
         final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
@@ -354,12 +359,7 @@ public class RtpContentMap {
 
     public RtpContentMap toContentModification(final Collection<String> modifications) {
         return new RtpContentMap(
-                this.group,
-                Maps.transformValues(
-                        Maps.filterKeys(contents, Predicates.in(modifications)),
-                        dt ->
-                                new DescriptionTransport(
-                                        dt.senders, dt.description, IceUdpTransportInfo.STUB)));
+                this.group, Maps.filterKeys(contents, Predicates.in(modifications)));
     }
 
     public RtpContentMap toStub() {
@@ -396,37 +396,43 @@ public class RtpContentMap {
     }
 
     public RtpContentMap addContent(
-            final RtpContentMap modification, final IceUdpTransportInfo.Setup setup) {
-        final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials();
-        final Collection<String> iceOptions = getCombinedIceOptions();
-        final DTLS dtls = getDistinctDtls();
+            final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
         final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
         final Map<String, DescriptionTransport> combinedFixedTransport =
                 Maps.transformValues(
                         combined,
                         dt -> {
                             final IceUdpTransportInfo iceUdpTransportInfo;
-                            if (dt.transport.emptyCredentials()) {
+                            if (dt.transport.isStub()) {
+                                final IceUdpTransportInfo.Credentials credentials =
+                                        getDistinctCredentials();
+                                final Collection<String> iceOptions = getCombinedIceOptions();
+                                final DTLS dtls = getDistinctDtls();
                                 iceUdpTransportInfo =
                                         IceUdpTransportInfo.of(
                                                 credentials,
                                                 iceOptions,
-                                                setup,
+                                                setupOverwrite,
                                                 dtls.hash,
                                                 dtls.fingerprint);
                             } else {
+                                final IceUdpTransportInfo.Fingerprint fp =
+                                        dt.transport.getFingerprint();
+                                final IceUdpTransportInfo.Setup setup = fp.getSetup();
                                 iceUdpTransportInfo =
                                         IceUdpTransportInfo.of(
                                                 dt.transport.getCredentials(),
-                                                iceOptions,
-                                                setup,
-                                                dtls.hash,
-                                                dtls.fingerprint);
+                                                dt.transport.getIceOptions(),
+                                                setup == IceUdpTransportInfo.Setup.ACTPASS
+                                                        ? setupOverwrite
+                                                        : setup,
+                                                fp.getHash(),
+                                                fp.getContent());
                             }
                             return new DescriptionTransport(
                                     dt.senders, dt.description, iceUdpTransportInfo);
                         });
-        return new RtpContentMap(modification.group, combinedFixedTransport);
+        return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
     }
 
     private static Map<String, DescriptionTransport> merge(

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

@@ -176,8 +176,15 @@ public class SessionDescription {
             mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions));
             final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
             if (fingerprint != null) {
-                mediaAttributes.put(
-                        "fingerprint", fingerprint.getHash() + " " + fingerprint.getContent());
+                final String hashFunction = fingerprint.getHash();
+                final String hash = fingerprint.getContent();
+                if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) {
+                    throw new IllegalArgumentException("DTLS-SRTP missing hash");
+                }
+                checkNoWhitespace(
+                        hashFunction, "DTLS-SRTP hash function must not contain whitespace");
+                checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace");
+                mediaAttributes.put("fingerprint", hashFunction + " " + hash);
                 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
                 if (setup != null) {
                     mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
@@ -214,12 +221,14 @@ public class SessionDescription {
                     }
                     checkNoWhitespace(
                             type, "feedback negotiation type must not contain whitespace");
-                    mediaAttributes.put(
-                            "rtcp-fb",
-                            id
-                                    + " "
-                                    + type
-                                    + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
+                    if (Strings.isNullOrEmpty(subtype)) {
+                        mediaAttributes.put("rtcp-fb", id + " " + type);
+                    } else {
+                        checkNoWhitespace(
+                                subtype,
+                                "feedback negotiation subtype must not contain whitespace");
+                        mediaAttributes.put("rtcp-fb", id + " " + type + " " + subtype);
+                    }
                 }
                 for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt :
                         payloadType.feedbackNegotiationTrrInts()) {
@@ -236,9 +245,13 @@ public class SessionDescription {
                     throw new IllegalArgumentException("a feedback negotiation is missing type");
                 }
                 checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
-                mediaAttributes.put(
-                        "rtcp-fb",
-                        "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
+                if (Strings.isNullOrEmpty(subtype)) {
+                    mediaAttributes.put("rtcp-fb", "* " + type);
+                } else {
+                    checkNoWhitespace(
+                            subtype, "feedback negotiation subtype must not contain whitespace");
+                    mediaAttributes.put("rtcp-fb", "* " + type + " " + subtype); /**/
+                }
             }
             for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt :
                     description.feedbackNegotiationTrrInts()) {
@@ -275,6 +288,9 @@ public class SessionDescription {
                 if (groups.size() == 0) {
                     throw new IllegalArgumentException("A SSRC group is missing SSRC ids");
                 }
+                for (final String source : groups) {
+                    checkNoWhitespace(source, "Sources must not contain whitespace");
+                }
                 mediaAttributes.put(
                         "ssrc-group",
                         String.format("%s %s", semantics, Joiner.on(' ').join(groups)));
@@ -298,7 +314,14 @@ public class SessionDescription {
                         throw new IllegalArgumentException(
                                 "A source specific media attribute is missing its value");
                     }
-                    mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue);
+                    checkNoWhitespace(
+                            parameterName,
+                            "A source specific media attribute name not not contain whitespace");
+                    checkNoNewline(
+                            parameterValue,
+                            "A source specific media attribute value must not contain new lines");
+                    mediaAttributes.put(
+                            "ssrc", id + " " + parameterName + ":" + parameterValue.trim());
                 }
             }
 
@@ -337,6 +360,13 @@ public class SessionDescription {
         return input;
     }
 
+    public static String checkNoNewline(final String input, final String message) {
+        if (CharMatcher.anyOf("\r\n").matchesAnyOf(message)) {
+            throw new IllegalArgumentException(message);
+        }
+        return input;
+    }
+
     public static int ignorantIntParser(final String input) {
         try {
             return Integer.parseInt(input);

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

@@ -60,6 +60,8 @@ public class WebRTCWrapper {
     private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
 
     private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+    private final ExecutorService localDescriptionExecutorService =
+            Executors.newSingleThreadExecutor();
 
     private static final int TONE_DURATION = 500;
     private static final Map<String,Integer> TONE_CODES;
@@ -79,8 +81,6 @@ public class WebRTCWrapper {
         builder.put("#", ToneGenerator.TONE_DTMF_P);
         TONE_CODES = builder.build();
     }
-    private final ExecutorService localDescriptionExecutorService =
-            Executors.newSingleThreadExecutor();
 
     private static final Set<String> HARDWARE_AEC_BLACKLIST =
             new ImmutableSet.Builder<String>()
@@ -95,6 +95,7 @@ public class WebRTCWrapper {
                     .add("E5823") // Sony z5 compact
                     .add("Redmi Note 5")
                     .add("FP2") // Fairphone FP2
+                    .add("FP4") // Fairphone FP4
                     .add("MI 5")
                     .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte)
                     .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte)

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java 🔗

@@ -85,7 +85,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
         iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
         iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);
         iceUdpTransportInfo.setAttribute("pwd", credentials.password);
-        for(final String iceOption : iceOptions) {
+        for (final String iceOption : iceOptions) {
             iceUdpTransportInfo.addChild(new IceOption(iceOption));
         }
         return iceUdpTransportInfo;
@@ -113,8 +113,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
         return new Credentials(ufrag, password);
     }
 
-    public boolean emptyCredentials() {
-        return Strings.isNullOrEmpty(this.getAttribute("ufrag")) || Strings.isNullOrEmpty(this.getAttribute("pwd"));
+    public boolean isStub() {
+        return Strings.isNullOrEmpty(this.getAttribute("ufrag"))
+                && Strings.isNullOrEmpty(this.getAttribute("pwd"))
+                && getChildren().isEmpty();
     }
 
     public List<Candidate> getCandidates() {

src/main/res/drawable-v24/ic_launcher_background.xml 🔗

@@ -0,0 +1,33 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group android:scaleX="0.68"
+      android:scaleY="0.68"
+      android:translateX="17.28"
+      android:translateY="17.28">
+    <path
+        android:pathData="M0,0h108v108h-108z"
+        android:fillColor="#4BAE4F"/>
+    <group>
+      <clip-path
+          android:pathData="M0,0h108v108h-108z"/>
+      <path
+          android:pathData="M176.75,102.5m-143.5,0a143.5,143.5 0,1 1,287 0a143.5,143.5 0,1 1,-287 0">
+        <aapt:attr name="android:fillColor">
+          <gradient 
+              android:startX="176.75"
+              android:startY="-41"
+              android:endX="176.75"
+              android:endY="246"
+              android:type="linear">
+            <item android:offset="0.11" android:color="#0CD9D9D9"/>
+            <item android:offset="0.94" android:color="#7FFFFFFF"/>
+          </gradient>
+        </aapt:attr>
+      </path>
+    </group>
+  </group>
+</vector>

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

@@ -0,0 +1,5 @@
+<vector android:autoMirrored="true" android:height="24dp"
+    android:tint="#FFFFFF" android:viewportHeight="24"
+    android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
+</vector>

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

@@ -0,0 +1,6 @@
+<vector android:height="48dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M18,11c0.34,0 0.67,0.03 1,0.08V4c0,-1.1 -0.9,-2 -2,-2H5C3.9,2 3,2.9 3,4v16c0,1.1 0.9,2 2,2h7.26C11.47,20.87 11,19.49 11,18C11,14.13 14.13,11 18,11zM7,11V4h5v7L9.5,9.5L7,11z"/>
+    <path android:fillColor="@android:color/white" android:pathData="M18,13c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S20.76,13 18,13zM16.75,20.5v-5l4,2.5L16.75,20.5z"/>
+</vector>

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

@@ -0,0 +1,6 @@
+<vector android:height="48dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M18,11c0.34,0 0.67,0.03 1,0.08V4c0,-1.1 -0.9,-2 -2,-2H5C3.9,2 3,2.9 3,4v16c0,1.1 0.9,2 2,2h7.26C11.47,20.87 11,19.49 11,18C11,14.13 14.13,11 18,11zM7,11V4h5v7L9.5,9.5L7,11z"/>
+    <path android:fillColor="@android:color/white" android:pathData="M18,13c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S20.76,13 18,13zM16.75,20.5v-5l4,2.5L16.75,20.5z"/>
+</vector>

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

@@ -0,0 +1,40 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="@color/black"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,19h2v2h-2z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M13,13h2v2h-2z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M15,15h2v2h-2z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M13,17h2v2h-2z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M15,19h2v2h-2z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M17,17h2v2h-2z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M17,13h2v2h-2z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,15h2v2h-2z" />
+</vector>

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

@@ -0,0 +1,40 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="@color/white">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19,19h2v2h-2z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13,13h2v2h-2z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M15,15h2v2h-2z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13,17h2v2h-2z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M15,19h2v2h-2z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17,17h2v2h-2z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17,13h2v2h-2z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19,15h2v2h-2z"/>
+</vector>

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

@@ -215,6 +215,19 @@
                             android:orientation="vertical"
                             android:padding="@dimen/card_padding_list"/>
 
+                        <LinearLayout
+                            android:id="@+id/unverified_warning"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:paddingHorizontal="@dimen/card_padding_list">
+                            <TextView
+                                android:layout_marginHorizontal="@dimen/list_padding"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary"
+                                android:text="@string/contact_uses_unverified_keys"/>
+                        </LinearLayout>
+
                         <LinearLayout
                             android:layout_width="wrap_content"
                             android:layout_height="match_parent"

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

@@ -1,14 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:app="http://schemas.android.com/apk/res-auto"
-        xmlns:tools="http://schemas.android.com/tools">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:background="?attr/color_background_secondary">
 
-        <include android:id="@+id/toolbar"
+        <include
+            android:id="@+id/toolbar"
             layout="@layout/toolbar" />
 
         <ScrollView
@@ -28,10 +29,10 @@
                     android:id="@+id/editor"
                     android:layout_width="fill_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                    android:layout_marginTop="@dimen/activity_vertical_margin"
                     android:layout_marginRight="@dimen/activity_horizontal_margin"
-                    android:layout_marginTop="@dimen/activity_vertical_margin">
+                    android:layout_marginBottom="@dimen/activity_vertical_margin">
 
                     <RelativeLayout
                         android:layout_width="match_parent"
@@ -59,17 +60,17 @@
                                 android:layout_width="match_parent"
                                 android:layout_height="wrap_content"
                                 android:hint="@string/account_settings_jabber_id"
-                                app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
-                                app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error">
+                                app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"
+                                app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint">
 
                                 <AutoCompleteTextView
                                     android:id="@+id/account_jid"
+                                    style="@style/Widget.Conversations.EditText"
                                     android:layout_width="match_parent"
                                     android:layout_height="wrap_content"
                                     android:imeOptions="actionNext"
                                     android:inputType="textEmailAddress"
-                                    android:textColor="?attr/edit_text_color"
-                                    style="@style/Widget.Conversations.EditText"/>
+                                    android:textColor="?attr/edit_text_color" />
                             </com.google.android.material.textfield.TextInputLayout>
 
 
@@ -77,21 +78,21 @@
                                 android:id="@+id/account_password_layout"
                                 android:layout_width="match_parent"
                                 android:layout_height="wrap_content"
+                                app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"
+                                app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
                                 app:passwordToggleDrawable="@drawable/visibility_toggle_drawable"
                                 app:passwordToggleEnabled="true"
-                                app:passwordToggleTint="?android:textColorSecondary"
-                                app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
-                                app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error">
+                                app:passwordToggleTint="?android:textColorSecondary">
 
                                 <eu.siacs.conversations.ui.widget.TextInputEditText
                                     android:id="@+id/account_password"
+                                    style="@style/Widget.Conversations.EditText"
                                     android:layout_width="match_parent"
                                     android:layout_height="wrap_content"
                                     android:layout_alignParentTop="true"
                                     android:hint="@string/password"
                                     android:inputType="textPassword"
-                                    android:textColor="?attr/edit_text_color"
-                                    style="@style/Widget.Conversations.EditText"/>
+                                    android:textColor="?attr/edit_text_color" />
                             </com.google.android.material.textfield.TextInputLayout>
 
                             <LinearLayout
@@ -113,15 +114,15 @@
                                         android:layout_width="match_parent"
                                         android:layout_height="wrap_content"
                                         android:hint="@string/account_settings_hostname"
-                                        app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
-                                        app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error">
+                                        app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"
+                                        app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint">
 
                                         <EditText
                                             android:id="@+id/hostname"
+                                            style="@style/Widget.Conversations.EditText"
                                             android:layout_width="fill_parent"
                                             android:layout_height="wrap_content"
-                                            android:inputType="textWebEmailAddress"
-                                            style="@style/Widget.Conversations.EditText"/>
+                                            android:inputType="textWebEmailAddress" />
                                     </com.google.android.material.textfield.TextInputLayout>
                                 </LinearLayout>
 
@@ -136,16 +137,16 @@
                                         android:layout_width="match_parent"
                                         android:layout_height="wrap_content"
                                         android:hint="@string/account_settings_port"
-                                        app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
-                                        app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error">
+                                        app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"
+                                        app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint">
 
                                         <EditText
                                             android:id="@+id/port"
+                                            style="@style/Widget.Conversations.EditText"
                                             android:layout_width="match_parent"
                                             android:layout_height="match_parent"
                                             android:inputType="number"
-                                            android:maxLength="5"
-                                            style="@style/Widget.Conversations.EditText"/>
+                                            android:maxLength="5" />
                                     </com.google.android.material.textfield.TextInputLayout>
                                 </LinearLayout>
                             </LinearLayout>
@@ -156,7 +157,7 @@
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:layout_marginTop="8dp"
-                                android:text="@string/register_account"/>
+                                android:text="@string/register_account" />
                         </LinearLayout>
                     </RelativeLayout>
                 </androidx.cardview.widget.CardView>
@@ -165,10 +166,10 @@
                     android:id="@+id/os_optimization"
                     android:layout_width="fill_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:layout_marginLeft="@dimen/activity_horizontal_margin"
-                    android:layout_marginRight="@dimen/activity_horizontal_margin"
                     android:layout_marginTop="@dimen/activity_vertical_margin"
+                    android:layout_marginRight="@dimen/activity_horizontal_margin"
+                    android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:visibility="gone">
 
                     <LinearLayout
@@ -187,7 +188,7 @@
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:text="@string/battery_optimizations_enabled"
-                                android:textAppearance="@style/TextAppearance.Conversations.Title"/>
+                                android:textAppearance="@style/TextAppearance.Conversations.Title" />
 
                             <TextView
                                 android:id="@+id/os_optimization_body"
@@ -195,7 +196,7 @@
                                 android:layout_height="wrap_content"
                                 android:layout_marginTop="8dp"
                                 android:text="@string/battery_optimizations_enabled_explained"
-                                android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
                         </LinearLayout>
 
                         <LinearLayout
@@ -213,7 +214,7 @@
                                 android:paddingLeft="16dp"
                                 android:paddingRight="16dp"
                                 android:text="@string/disable"
-                                android:textColor="?colorAccent"/>
+                                android:textColor="?colorAccent" />
                         </LinearLayout>
                     </LinearLayout>
                 </androidx.cardview.widget.CardView>
@@ -223,10 +224,10 @@
                     android:id="@+id/stats"
                     android:layout_width="fill_parent"
                     android:layout_height="fill_parent"
-                    android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:layout_marginLeft="@dimen/activity_horizontal_margin"
-                    android:layout_marginRight="@dimen/activity_horizontal_margin"
                     android:layout_marginTop="@dimen/activity_vertical_margin"
+                    android:layout_marginRight="@dimen/activity_horizontal_margin"
+                    android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:visibility="gone">
 
                     <LinearLayout
@@ -252,7 +253,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_session_established"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/session_est"
@@ -260,7 +261,7 @@
                                     android:layout_height="wrap_content"
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
                             </TableRow>
 
                         </TableLayout>
@@ -283,7 +284,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_pep"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_pep"
@@ -292,7 +293,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -305,7 +306,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_blocking"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_blocking"
@@ -314,7 +315,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -327,7 +328,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_stream_management"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_sm"
@@ -336,7 +337,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -349,7 +350,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_external_service_discovery"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_external_service"
@@ -358,7 +359,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -371,7 +372,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_roster_version"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_roster_version"
@@ -380,7 +381,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -393,7 +394,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_carbon_messages"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_carbons"
@@ -402,7 +403,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -415,7 +416,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_mam"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_mam"
@@ -424,7 +425,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -437,7 +438,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_csi"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_csi"
@@ -446,7 +447,7 @@
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
                                     android:textAppearance="@style/TextAppearance.Conversations.Body1"
-                                    tools:ignore="RtlHardcoded"/>
+                                    tools:ignore="RtlHardcoded" />
                             </TableRow>
 
                             <TableRow
@@ -460,7 +461,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_push"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_push"
@@ -468,7 +469,7 @@
                                     android:layout_height="wrap_content"
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
                             </TableRow>
 
                             <TableRow
@@ -482,7 +483,7 @@
                                     android:ellipsize="end"
                                     android:singleLine="true"
                                     android:text="@string/server_info_http_upload"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                                 <TextView
                                     android:id="@+id/server_info_http_upload"
@@ -490,7 +491,7 @@
                                     android:layout_height="wrap_content"
                                     android:layout_gravity="right"
                                     android:paddingLeft="4dp"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1" />
                             </TableRow>
                         </TableLayout>
 
@@ -513,14 +514,14 @@
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
                                     android:text="@string/no_name_set_instructions"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Body1.Tertiary"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1.Tertiary" />
 
                                 <TextView
                                     android:id="@+id/your_name_desc"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
                                     android:text="@string/your_name"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Caption"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Caption" />
                             </LinearLayout>
 
                             <ImageButton
@@ -533,7 +534,7 @@
                                 android:background="?attr/selectableItemBackgroundBorderless"
                                 android:padding="@dimen/image_button_padding"
                                 android:src="?attr/icon_edit_body"
-                                android:visibility="visible"/>
+                                android:visibility="visible" />
                         </RelativeLayout>
 
                         <RelativeLayout
@@ -639,14 +640,14 @@
                                     android:id="@+id/pgp_fingerprint"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Fingerprint"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Fingerprint" />
 
                                 <TextView
                                     android:id="@+id/pgp_fingerprint_desc"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
                                     android:text="@string/openpgp_key_id"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Caption"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Caption" />
                             </LinearLayout>
 
                             <ImageButton
@@ -659,7 +660,7 @@
                                 android:background="?attr/selectableItemBackgroundBorderless"
                                 android:padding="@dimen/image_button_padding"
                                 android:src="?attr/icon_remove"
-                                android:visibility="visible"/>
+                                android:visibility="visible" />
                         </RelativeLayout>
 
                         <RelativeLayout
@@ -680,13 +681,13 @@
                                     android:id="@+id/axolotl_fingerprint"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Fingerprint"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Fingerprint" />
 
                                 <TextView
                                     android:id="@+id/own_fingerprint_desc"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:textAppearance="@style/TextAppearance.Conversations.Caption"/>
+                                    android:textAppearance="@style/TextAppearance.Conversations.Caption" />
                             </LinearLayout>
 
                             <LinearLayout
@@ -698,15 +699,15 @@
                                 android:orientation="vertical">
 
                                 <ImageButton
-                                    android:id="@+id/action_copy_axolotl_to_clipboard"
+                                    android:id="@+id/show_qr_code_button"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
                                     android:alpha="?attr/icon_alpha"
                                     android:background="?attr/selectableItemBackgroundBorderless"
                                     android:contentDescription="@string/copy_omemo_clipboard_description"
                                     android:padding="@dimen/image_button_padding"
-                                    android:src="?attr/icon_copy"
-                                    android:visibility="visible"/>
+                                    android:src="?attr/icon_qr_code"
+                                    android:visibility="visible" />
 
                                 <ImageButton
                                     android:id="@+id/action_regenerate_axolotl_key"
@@ -717,7 +718,7 @@
                                     android:contentDescription="@string/regenerate_omemo_key"
                                     android:padding="@dimen/image_button_padding"
                                     android:src="?attr/icon_refresh"
-                                    android:visibility="gone"/>
+                                    android:visibility="gone" />
 
                             </LinearLayout>
                         </RelativeLayout>
@@ -728,41 +729,83 @@
                     android:id="@+id/other_device_keys_card"
                     android:layout_width="fill_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:layout_marginLeft="@dimen/activity_horizontal_margin"
-                    android:layout_marginRight="@dimen/activity_horizontal_margin"
                     android:layout_marginTop="@dimen/activity_vertical_margin"
+                    android:layout_marginRight="@dimen/activity_horizontal_margin"
+                    android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:visibility="gone">
 
                     <LinearLayout
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
-                        android:orientation="vertical"
-                        android:padding="@dimen/card_padding_list">
+                        android:orientation="vertical">
 
-                        <TextView
-                            android:id="@+id/other_device_keys_title"
-                            android:layout_width="wrap_content"
+                        <LinearLayout
+                            android:layout_width="match_parent"
                             android:layout_height="wrap_content"
-                            android:layout_margin="@dimen/list_padding"
-                            android:text="@string/other_devices"
-                            android:textAppearance="@style/TextAppearance.Conversations.Title"/>
+                            android:orientation="vertical"
+                            android:padding="@dimen/card_padding_list">
+
+                            <TextView
+                                android:id="@+id/other_device_keys_title"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_margin="@dimen/list_padding"
+                                android:text="@string/other_devices"
+                                android:textAppearance="@style/TextAppearance.Conversations.Title" />
+
+                            <LinearLayout
+                                android:id="@+id/other_device_keys"
+                                android:layout_width="fill_parent"
+                                android:layout_height="wrap_content"
+                                android:orientation="vertical" />
+                        </LinearLayout>
 
                         <LinearLayout
-                            android:id="@+id/other_device_keys"
-                            android:layout_width="fill_parent"
+                            android:id="@+id/unverified_warning"
+                            android:layout_width="match_parent"
                             android:layout_height="wrap_content"
-                            android:orientation="vertical"/>
+                            android:paddingHorizontal="@dimen/card_padding_list">
 
-                        <Button
-                            android:id="@+id/clear_devices"
-                            style="@style/Widget.Conversations.Button.Borderless"
+                            <TextView
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_marginHorizontal="@dimen/list_padding"
+                                android:text="@string/unverified_devices"
+                                android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary" />
+                        </LinearLayout>
+
+                        <LinearLayout
                             android:layout_width="wrap_content"
-                            android:layout_height="wrap_content"
-                            android:layout_gravity="center_horizontal"
-                            android:text="@string/clear_other_devices"
-                            android:textColor="?colorAccent"/>
+                            android:layout_height="match_parent"
+                            android:layout_marginTop="8dp"
+                            android:orientation="horizontal">
+
+
+                            <Button
+                                android:id="@+id/scan_button"
+                                style="@style/Widget.Conversations.Button.Borderless"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:minWidth="0dp"
+                                android:paddingLeft="16dp"
+                                android:paddingRight="16dp"
+                                android:text="@string/scan_qr_code"
+                                android:textColor="?attr/colorAccent" />
+
+                            <Button
+                                android:id="@+id/clear_devices"
+                                style="@style/Widget.Conversations.Button.Borderless"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:minWidth="0dp"
+                                android:paddingLeft="16dp"
+                                android:paddingRight="16dp"
+                                android:text="@string/clear_other_devices"
+                                android:textColor="?attr/colorAccent" />
+                        </LinearLayout>
                     </LinearLayout>
+
                 </androidx.cardview.widget.CardView>
             </LinearLayout>
         </ScrollView>
@@ -771,11 +814,11 @@
             android:id="@+id/button_bar"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_alignParentBottom="true"
-            android:layout_alignParentEnd="true"
+            android:layout_alignParentStart="true"
             android:layout_alignParentLeft="true"
+            android:layout_alignParentEnd="true"
             android:layout_alignParentRight="true"
-            android:layout_alignParentStart="true">
+            android:layout_alignParentBottom="true">
 
             <Button
                 android:id="@+id/cancel_button"
@@ -783,14 +826,14 @@
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_weight="1"
-                android:text="@string/cancel"/>
+                android:text="@string/cancel" />
 
             <View
                 android:layout_width="1dp"
                 android:layout_height="fill_parent"
-                android:layout_marginBottom="7dp"
                 android:layout_marginTop="7dp"
-                android:background="?attr/divider"/>
+                android:layout_marginBottom="7dp"
+                android:background="?attr/divider" />
 
             <Button
                 android:id="@+id/save_button"
@@ -799,7 +842,7 @@
                 android:layout_height="wrap_content"
                 android:layout_weight="1"
                 android:enabled="false"
-                android:text="@string/save"/>
+                android:text="@string/save" />
         </LinearLayout>
 
     </RelativeLayout>

src/conversations/res/mipmap-anydpi-v26/new_launcher.xml → src/main/res/mipmap-anydpi-v26/new_launcher.xml 🔗

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@mipmap/ic_launcher_background"/>
-    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_monochrome" />
 </adaptive-icon>

src/conversations/res/mipmap-anydpi-v26/new_launcher_round.xml → src/main/res/mipmap-anydpi-v26/new_launcher_round.xml 🔗

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@mipmap/ic_launcher_background"/>
+    <background android:drawable="@drawable/ic_launcher_background"/>
     <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+    <monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
 </adaptive-icon>

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

@@ -619,7 +619,7 @@
     <string name="ebook">كتاب إلكتروني</string>
     <string name="video_original">أصلي (غير مضغوط)</string>
     <string name="open_with">إفتح بـ...</string>
-    <string name="set_profile_picture">صورة حساب كونفرسايشنز</string>
+    <string name="set_profile_picture">صورة حساب Conversations</string>
     <string name="choose_account">إختيار الحساب</string>
     <string name="restore_backup">استرجِع نسخة احتياطية</string>
     <string name="restore">استرجِع</string>
@@ -650,4 +650,23 @@
     <string name="please_enter_password">يرجى إدخال الكلمة السرية للحساب</string>
     <string name="rtp_state_declined_or_busy">مشغول</string>
     <string name="more_options">خيارات أخرى</string>
-    </resources>
+    <string name="also_end_conversation">قم بإنهاء هذه المحادثة بعد ذلك</string>
+    <string name="huawei_protected_apps_summary">للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف تطبيق Conversations إلى قائمة التطبيقات المحميّة.</string>
+    <string name="account_status_regis_not_sup">إنشاء الحسابات غير مدعومة مِن طرف الخادم</string>
+    <string name="error_publish_avatar_no_server_support">خادمك لا يدعم نشر الصور الرمزية</string>
+    <string name="server_info_external_service_discovery">XEP-0215: استكشاف خدمة خارجية</string>
+    <string name="group_chats">محادثات جماعية</string>
+    <string name="pref_never_send_crash_summary">عبر إرسال أثار الأخطاء تقوم بالمساعدة في التطوير</string>
+    <string name="touch_to_choose_picture">اضغط على الصورة الرمزية لاختيار صورة مِن المعرض</string>
+    <string name="pref_call_ringtone_summary">نغمة المكالمات الواردة</string>
+    <string name="touch_to_fix">اضغط لإدارة حساباتك</string>
+    <string name="openpgp_error">لقد أَبلَغَ OpenKeychain عند حدوث خطأ.</string>
+    <string name="openpgp_has_been_published">تم نشر مفتاح OpenPGP العمومي.</string>
+    <string name="pref_prevent_screenshots">منع أخذ لقطات للشاشة</string>
+    <string name="last_seen_day">آخِر ظهور البارحة</string>
+    <string name="last_seen_hour">آخر ظهور منذ ساعة</string>
+    <string name="save_as_group_chat">احفظه كمحادثة جماعية</string>
+    <string name="account_status_regis_invalid_token">رمز التسجيل غير صالح</string>
+    <string name="bad_key_for_encryption">مفتاح تعمية خاطئ.</string>
+    <string name="invalid_jid">هذا ليس عنوان XMPP صالح</string>
+</resources>

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

@@ -31,10 +31,7 @@
     <string name="minutes_ago">fa %d mins</string>
     <plurals name="x_unread_conversations">
         <item quantity="one">%dconverses no llegides </item>
-
-    
         <item quantity="other">%d converses no llegides</item>
-
     </plurals>
     <string name="sending">enviant…</string>
     <string name="message_decrypting">Desxifrant el missatge. Espereu…</string>
@@ -87,7 +84,9 @@
     <string name="clear_conversation_history">Neteja l\'historial de la conversa</string>
     <string name="clear_histor_msg">Vols esborrar tots els missatges d\'aquesta conversa?\n\n<b>  Advertiment: </b> Això no influirà en els missatges emmagatzemats en altres dispositius o servidors.</string>
     <string name="delete_file_dialog">Eliminar fitxer</string>
-    <string name="delete_file_dialog_msg">Estàs segur que vols esborrar aquest fitxer?\n\n <b> Advertiment: </b> Això no eliminarà les còpies d\'aquest fitxer que estiguin emmagatzemades en altres dispositius o servidors.</string>
+    <string name="delete_file_dialog_msg">Estàs segur que vols esborrar aquest fitxer\?
+\n
+\n<b>Advertiment:</b> Això no eliminarà les còpies d\'aquest fitxer que estiguin emmagatzemades en altres dispositius o servidors. </string>
     <string name="also_end_conversation">Tanca aquesta conversa després</string>
     <string name="choose_presence">Tria el dispositiu</string>
     <string name="send_unencrypted_message">Envia un missatge no xifrat</string>
@@ -108,7 +107,9 @@
     <string name="no_pgp_key">No s\'ha trobat cap clau OpenPGP</string>
     <string name="contact_has_no_pgp_key">No es va poder xifrar el teu missatge perquè el teu contacte no està anunciant la seva clau pública.\n\n<small> Si us plau, demana-li al seu contacte que configuri OpenPGP.</small></string>
     <string name="no_pgp_keys">No s\'ha trobat cap clau OpenPGP</string>
-    <string name="contacts_have_no_pgp_keys">No es pot encriptar el teu missatge perquè els teus contactes no anuncien les seves claus públiques. Si us plau, demana\'ls que configurin OpenPGP.</string>
+    <string name="contacts_have_no_pgp_keys">No es pot encriptar el teu missatge perquè els teus contactes no anuncien les seves claus públiques.
+\n
+\n<small>Si us plau, demana\'ls que configurin OpenPGP.</small></string>
     <string name="pref_general">General</string>
     <string name="pref_accept_files">Accepta els fitxers</string>
     <string name="pref_accept_files_summary">Accepta fitxers automàticament amb una mida menor a…</string>
@@ -144,8 +145,10 @@
     <string name="error_not_an_image_file">El fitxer triat no és una imatge</string>
     <string name="error_compressing_image">No es va poder convertir l\'arxiu d\'imatge</string>
     <string name="error_file_not_found">No s\'ha trobat el fitxer</string>
-    <string name="error_io_exception">Error d\'E/S general. Pot ser que us hagueu quedat sense espai d\'emmagatzematge.</string>
-    <string name="error_security_exception_during_image_copy">L\'aplicació que vas usar per a seleccionar aquesta imatge no va proporcionar suficients permisos per a llegir l\'arxiu.\n\n<small>Utilitza un gestor d\'arxius diferent per a triar una imatge</small>. </string>
+    <string name="error_io_exception">Error d\'E/S general. Pot ser que us hagueu quedat sense espai d\'emmagatzematge\?</string>
+    <string name="error_security_exception_during_image_copy">L\'aplicació que vas usar per a seleccionar aquesta imatge no va proporcionar suficients permisos per a llegir l\'arxiu.
+\n
+\n<small>Utilitza un gestor d\'arxius diferent per a triar una imatge</small>.</string>
     <string name="account_status_unknown">Desconegut</string>
     <string name="account_status_disabled">Inhabilitat temporalment</string>
     <string name="account_status_online">En línia</string>
@@ -266,7 +269,9 @@
     <string name="enable">Habilita</string>
     <string name="conference_requires_password">El xat de grup requereix contrasenya</string>
     <string name="enter_password">Introduïu la contrasenya</string>
-    <string name="request_presence_updates">Si us plau, sol·liciti primer les actualitzacions de presència al seu contacte.\n\n<small>Això s\'usarà per a determinar quina aplicació de xat està usant el teu contacte</small>. </string>
+    <string name="request_presence_updates">Si us plau, sol·liciti primer les actualitzacions de presència al seu contacte.
+\n
+\n<small>Això s\'usarà per a determinar quina aplicació de xat està usant el teu contacte</small>.</string>
     <string name="request_now">Sol·licita ara</string>
     <string name="ignore">Ignora</string>
     <string name="without_mutual_presence_updates"><b>Advertiment::</b> Enviar això sense actualitzacions de presència mútua podria causar problemes inesperats.\n\n<small> Vagi a \"Dades de contacte\" per a verificar les seves subscripcions de presència.</small></string>
@@ -352,8 +357,8 @@
     <string name="error_trustkeys_title">Alguna cosa ha anat malament</string>
     <string name="fetching_history_from_server">Anar a cercar la història als servidors</string>
     <string name="no_more_history_on_server">No hi ha més histories al servidor</string>
-    <string name="updating">Actualitzant</string>
-    <string name="password_changed">Contrasenya canviada</string>
+    <string name="updating">Actualitzant…</string>
+    <string name="password_changed">Contrasenya canviada!</string>
     <string name="could_not_change_password">No s\'ha pogut canviar la contrasenya</string>
     <string name="change_password">Cambiar contrasenya</string>
     <string name="current_password">Contrasenya actual</string>
@@ -361,7 +366,7 @@
     <string name="password_should_not_be_empty">La contrasenya no pot estar buida</string>
     <string name="enable_all_accounts">Habilitar tots els comptes</string>
     <string name="disable_all_accounts">Deshabilitar tots els comptes</string>
-    <string name="perform_action_with">Realitzar l\'acció amb…</string>
+    <string name="perform_action_with">Realitzar l\'acció amb</string>
     <string name="no_affiliation">Cap afiliació</string>
     <string name="no_role">Fora de línia</string>
     <string name="outcast">Outcast</string>
@@ -387,7 +392,7 @@
     <string name="non_anonymous">Fer que les direccions XMPP siguin visibles per a qualsevol</string>
     <string name="moderated">Fer que el canal sigui moderat</string>
     <string name="you_are_not_participating">No esteu participant</string>
-    <string name="modified_conference_options">S\'han modificat les opcions de xat en grup.</string>
+    <string name="modified_conference_options">S\'han modificat les opcions de xat en grup!</string>
     <string name="could_not_modify_conference_options">No s\'han pogut modificar les opcions de xat de grup</string>
     <string name="never">Mai</string>
     <string name="until_further_notice">Fins nou avís</string>
@@ -472,9 +477,9 @@
     <string name="certificate_chain_is_not_trusted">Cadena de certificats no fiable</string>
     <string name="jid_does_not_match_certificate">La direcció XMPP no coincideix amb el certificat</string>
     <string name="action_renew_certificate">Renova el certificat</string>
-    <string name="error_fetching_omemo_key">S\'ha produït un error en obtenir la clau OMEMO!.</string>
+    <string name="error_fetching_omemo_key">S\'ha produït un error en obtenir la clau OMEMO!</string>
     <string name="verified_omemo_key_with_certificate">Clau OMEMO verificada amb certificat!</string>
-    <string name="device_does_not_support_certificates">El vostre dispositiu no admet la selecció de certificats de client.</string>
+    <string name="device_does_not_support_certificates">El vostre dispositiu no admet la selecció de certificats de client!</string>
     <string name="pref_connection_options">Connexió</string>
     <string name="pref_use_tor">Connectar mitjançant Tor</string>
     <string name="pref_use_tor_summary">Tunelar totes les connexions a través de la xarxa Tor. Requereix Orbot</string>
@@ -771,8 +776,8 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic
     <string name="abort_registration_procedure">Està segur que vol avortar el procediment de registre?</string>
     <string name="yes">Si</string>
     <string name="no">No</string>
-    <string name="verifying">Verificant...</string>
-    <string name="requesting_sms">Sol·licitud de SMS...</string>
+    <string name="verifying">Verificant…</string>
+    <string name="requesting_sms">Sol·licitud de SMS…</string>
     <string name="incorrect_pin">El pin que has introduït és incorrecte.</string>
     <string name="pin_expired">El pin que li hem enviat ha caducat.</string>
     <string name="unknown_api_error_network">Error de xarxa desconegut.</string>
@@ -801,7 +806,7 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic
     <string name="group_chat_will_make_your_jabber_id_public">Aquest canal farà pública la seva adreça XMPP</string>
     <string name="ebook">Llibre electrònic</string>
     <string name="video_original">Original (sense comprimir)</string>
-    <string name="open_with">Obrir amb...</string>
+    <string name="open_with">Obrir amb…</string>
     <string name="set_profile_picture">Foto de perfil de Conversations</string>
     <string name="choose_account">Triï el compte</string>
     <string name="restore_backup">Restaurar còpia de seguretat</string>
@@ -821,7 +826,7 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic
     <string name="please_enter_name">Indiqui un nom per al canal</string>
     <string name="please_enter_xmpp_address">Indiqui una direcció XMPP</string>
     <string name="this_is_an_xmpp_address">Aquesta és una direcció XMPP. Si us plau, proporcioni un nom.</string>
-    <string name="creating_channel">Creant un canal públic...</string>
+    <string name="creating_channel">Creant un canal públic…</string>
     <string name="channel_already_exists">Aquest canal ja existeix</string>
     <string name="joined_an_existing_channel">T\'has unit a un canal existent</string>
     <string name="unable_to_set_channel_configuration">No s\'ha pogut guardar la configuració del canal</string>
@@ -842,7 +847,7 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic
     <string name="attach">Adjuntar</string>
     <string name="discover_channels">Descobreix canals</string>
     <string name="search_channels">Buscar canals</string>
-    <string name="channel_discovery_opt_in_title">Possible violació de la privacitat.</string>
+    <string name="channel_discovery_opt_in_title">Possible violació de la privacitat!</string>
     <string name="i_already_have_an_account">Ja tinc un compte</string>
     <string name="add_existing_account">Afegir compte existent</string>
     <string name="register_new_account">Registrar un nou compte</string>
@@ -857,7 +862,7 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic
     <string name="account_already_setup">Aquest compte ja està configurada</string>
     <string name="please_enter_password">Si us plau, introdueixi la contrasenya d\'aquest compte</string>
     <string name="unable_to_perform_this_action">No s\'ha pogut realitzar aquesta acció</string>
-    <string name="open_join_dialog">Unir-se al canal públic...</string>
+    <string name="open_join_dialog">Unir-se al canal públic…</string>
     <string name="sharing_application_not_grant_permission">L\'aplicació per a compartir no va donar permís per a accedir a aquest arxiu.</string>
     <string name="group_chats_and_channels"><![CDATA[Xats de grup i canals]]></string>
     <string name="jabber_network">jabber.network</string>
@@ -929,4 +934,4 @@ que l\'administrador del servidor llegeixi els missatges, però pot ser l\'únic
     <string name="unable_to_parse_invite">No es pot processar la invitació</string>
     <string name="server_does_not_support_easy_onboarding_invites">El servidor no admet la generació d\'invitacions</string>
     <string name="no_active_accounts_support_this">Cap compte actiu admet aquesta funció</string>
-    </resources>
+</resources>

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

@@ -103,7 +103,7 @@
     <string name="send_unencrypted">Poslat nešifrované</string>
     <string name="decryption_failed">Zašifrování se nezdařilo. Možná nemáte správný privátní klíč.</string>
     <string name="openkeychain_required">OpenKeychain</string>
-    <string name="openkeychain_required_long"><![CDATA[%1$s používá <b>OpenKeychain</b> k šifrování a dešifrování zpráv a ke správě Vašich veřejných klíčů.<br><br>OpenKeychain je vydána pod licencí GPLv3+ a dostupná na F-Droid nebo Google Play. <br><br><small>(Po instalaci, prosím, restartujte%1$s.)</small>]]></string>
+    <string name="openkeychain_required_long">%1$s používá &lt;b&gt;OpenKeychain&lt;/b&gt; k šifrování a dešifrování zpráv a ke správě Vašich veřejných klíčů.&lt;br&gt;&lt;br&gt;OpenKeychain je vydána pod licencí GPLv3+ a dostupná na F-Droid nebo Google Play.&lt;br&gt;&lt;br&gt;&lt;small&gt;(Po instalaci, prosím, restartujte %1$s.)&lt;/small&gt;</string>
     <string name="restart">Restartovat</string>
     <string name="install">Instalovat</string>
     <string name="openkeychain_not_installed">Nainstalujte prosím OpenKeychain</string>

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

@@ -264,7 +264,7 @@
     <string name="error_saving_avatar">Profilbild kann nicht gespeichert werden</string>
     <string name="or_long_press_for_default">(Oder klicke lange, um den Standard wiederherzustellen)</string>
     <string name="error_publish_avatar_no_server_support">Dein Server unterstützt die Veröffentlichung von Profilbildern nicht</string>
-    <string name="private_message">private Nachricht:</string>
+    <string name="private_message">geflüstert</string>
     <string name="private_message_to">an %s</string>
     <string name="send_private_message_to">Private Nachricht an %s senden</string>
     <string name="connect">Verbinden</string>
@@ -1007,4 +1007,8 @@
     <string name="save_as_group_chat">Als Gruppenchat speichern</string>
     <string name="search_group_chats">Gruppenchats durchsuchen</string>
     <string name="channel_discover_opt_in_message">Die Channelsuche verwendet einen Drittanbieterservice namens &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Wenn du diese Funktion verwendest, werden deine IP-Adresse und deine Suchbegriffe an diesen Dienst übertragen. Weitere Informationen findest du in der &lt;a href=https://search.jabber.network/privacy&gt;Datenschutzerklärung&lt;/a&gt;.</string>
+    <string name="restore_warning_continued">Versuche nicht, Backups wiederherzustellen, die du nicht selbst erstellt hast!</string>
+    <string name="outdated_backup_file_format">Du versuchst, ein veraltetes Sicherungsdateiformat zu importieren</string>
+    <string name="audiobook">Hörbuch</string>
+    <string name="reconnect_on_other_host">Verbindung auf anderem Host wiederherstellen</string>
 </resources>

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

@@ -589,7 +589,7 @@
     <string name="contact_asks_for_presence_subscription">El contacto solicita ver tus actualizaciones de estado</string>
     <string name="allow">Permitir</string>
     <string name="no_permission_to_access_x">Sin permiso de acceso a %s</string>
-    <string name="remote_server_not_found">Servidor no encontrado</string>
+    <string name="remote_server_not_found">No se encontró ningún servidor remoto</string>
     <string name="remote_server_timeout">Tiempo de espera agotado al servidor remoto</string>
     <string name="unable_to_update_account">No se ha podido actualizar la cuenta</string>
     <string name="report_jid_as_spammer">Reporta esta dirección XMPP como spam.</string>
@@ -1021,4 +1021,8 @@
     <string name="search_group_chats">Buscar un grupo de chats</string>
     <string name="channel_discover_opt_in_message">La búsqueda de canales utiliza un servicio de terceros denominado &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Si utiliza esta función, tu dirección IP y la búsqueda de términos serán transferidos a este servicio. Para obtener más información, consulta la &lt;a href=https://search.jabber.network/privacy&gt;Política de privacidad&lt;/a&gt;.</string>
     <string name="save_as_group_chat">Guardar como un chat en grupo</string>
+    <string name="restore_warning_continued">¡No intentes restaurar las copias de seguridad que no creaste tu mismo!</string>
+    <string name="outdated_backup_file_format">Estás intentando importar un formato de copia de seguridad obsoleto</string>
+    <string name="audiobook">Audiolibro</string>
+    <string name="reconnect_on_other_host">Reconectarse a otros hosts</string>
 </resources>

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

@@ -640,7 +640,7 @@
     <string name="choose_a_country">Herrialde bat hautatu</string>
     <string name="phone_number">telefono zenbakia</string>
     <string name="verify_your_phone_number">Zure telefono zenbakia egiaztatu</string>
-    <string name="enter_country_code_and_phone_number">Quicksyk SMS mezu bat bidali dizu (baliteke ordaindu behar izatea)  zure telefono zenbakia egiaztatzeko.</string>
+    <string name="enter_country_code_and_phone_number">Quicksyk SMS mezu bat bidali dizu (baliteke ordaindu behar izatea) zure telefono zenbakia egiaztatzeko. Idatzi zure herrialdeko kodea eta telefono zenbakia:</string>
     <string name="we_will_be_verifying"><![CDATA[<br/><br/><b>%s</b><br/><br/>telefono zenbakia egiaztatuko dugu. Zuzena da, edo zenbakia editatu nahiko al zenuke?]]></string>
     <string name="not_a_valid_phone_number">%s ez da telefono zenbaki baliodun bat.</string>
     <string name="please_enter_your_phone_number">Mesedez idatzi zure telefono zenbakia.</string>
@@ -749,4 +749,4 @@
         <item quantity="one">Parte-hartzaile %1$d ikusi</item>
         <item quantity="other">%1$d parte-hartzaile ikusi</item>
     </plurals>
-    </resources>
+</resources>

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

@@ -150,7 +150,9 @@
     <string name="error_compressing_image">Impossible de convertir l\'image</string>
     <string name="error_file_not_found">Impossible de trouver le fichier</string>
     <string name="error_io_exception">Erreur générale d\'E/S. Avez-vous encore de l\'espace libre ?</string>
-    <string name="error_security_exception_during_image_copy">L\'application utilisée ne donne pas la permission de lire l\'image.\n\n<small>Utilisez une autre application pour choisir une image.</small></string>
+    <string name="error_security_exception_during_image_copy">L\'application utilisée pour sélectionner cette image ne donne pas les autorisations nécessaires afin de lire le fichier.
+\n
+\n<small>Utilisez un autre gestionnaire de fichiers pour choisir une image</small>.</string>
     <string name="error_security_exception">L\'app avec laquelle vous avez partagé ce fichier n\'a pas fourni assez de permissions.</string>
     <string name="account_status_unknown">Inconnu</string>
     <string name="account_status_disabled">Désactivé temporairement</string>
@@ -182,7 +184,7 @@
     <string name="unpublish_pgp">Supprimer la clé publique OpenPGP</string>
     <string name="unpublish_pgp_message">Êtes-vous sûr de vouloir supprimer votre clé publique OpenPGP de votre annonce de présence ?\nVos contacts ne pourront plus vous envoyer de message chiffrés avec OpenPGP.</string>
     <string name="openpgp_has_been_published">Clé publique OpenPGP publiée.</string>
-    <string name="mgmt_account_enable">Activer</string>
+    <string name="mgmt_account_enable">Activer le compter</string>
     <string name="mgmt_account_delete_confirm_text">Êtes-vous sûr de vouloir supprimer votre compte \? Supprimer votre compte effacera l\'historique de vos conversations</string>
     <string name="attach_record_voice">Enregistrer un son</string>
     <string name="account_settings_jabber_id">Adresse XMPP</string>
@@ -276,7 +278,9 @@
     <string name="enable">Activer</string>
     <string name="conference_requires_password">Le groupe requiert un mot de passe</string>
     <string name="enter_password">Entrer le mot de passe</string>
-    <string name="request_presence_updates">Veuillez demander à votre contact de partager ses mises à jour de disponibilité.\n\n<small>Elles seront utilisées pour déterminer l\'application qu\'il utilise.</small></string>
+    <string name="request_presence_updates">Veuillez demander à votre contact de partager ses mises à jour de disponibilité.
+\n
+\n<small>Elles seront utilisées pour déterminer quelle application de discussion il utilise</small>.</string>
     <string name="request_now">Demander maintenant</string>
     <string name="ignore">Ignorer</string>
     <string name="without_mutual_presence_updates"><b>Attention :</b> peut poser problème si l\'un des deux correspondants n\'a pas activé les mises à jour de disponibilité.\n\n<small>Vérifiez dans « Détails du contact » que vous y avez bien souscrit.</small></string>
@@ -521,7 +525,9 @@
     <string name="large_images_only">Grandes images seulement</string>
     <string name="battery_optimizations_enabled">Optimisations de batterie activées</string>
     <string name="battery_optimizations_enabled_explained">Votre appareil applique d\'importantes optimisations de batterie pour %1$s pouvant entraîner des retards de notifications, voire des pertes de messages.\nIl est recommandé de les désactiver.</string>
-    <string name="battery_optimizations_enabled_dialog">Votre appareil applique d\'importantes optimisations de batterie pour %1$s pouvant entraîner des retards de notifications, voire des pertes de messages.\nVous allez être invité à les désactiver.</string>
+    <string name="battery_optimizations_enabled_dialog">Votre appareil applique d\'importantes optimisations de batterie pour %1$s pouvant entraîner des retards de notifications, voire des pertes de messages.
+\n
+\nVous allez être invité à les désactiver.</string>
     <string name="disable">Désactiver</string>
     <string name="selection_too_large">La zone sélectionnée est trop grande</string>
     <string name="no_accounts">(Aucun compte activé)</string>
@@ -783,7 +789,7 @@
     <string name="choose_a_country">Choisissez un pays</string>
     <string name="phone_number">Numéro de téléphone</string>
     <string name="verify_your_phone_number">Vérifier votre numéro de téléphone</string>
-    <string name="enter_country_code_and_phone_number">Quicksy va envoyer un message SMS (des frais opérateurs sont possibles) pour vérifier votre numéro de téléphone. Entrez votre code pays et votre No de téléphone.</string>
+    <string name="enter_country_code_and_phone_number">Quicksy va envoyer un message SMS (des frais opérateurs sont possibles) pour vérifier votre numéro de téléphone. Saisissez votre code de pays et votre numéro de téléphone :</string>
     <string name="we_will_be_verifying"><![CDATA[Nous vérifierons le numéro de téléphone<br/><br/><b>%s</b><br/><br/>. Est-ce correct ou souhaitez-vous modifier le numéro ?]]></string>
     <string name="not_a_valid_phone_number">%s n\'est pas un numéro de téléphone valide.</string>
     <string name="please_enter_your_phone_number">Veuillez saisir votre numéro de téléphone.</string>
@@ -1024,4 +1030,8 @@
     <string name="plain_text_document">Document texte</string>
     <string name="account_status_temporary_auth_failure">Échec temporaire de l\'authentification</string>
     <string name="delete_avatar">Supprimer l\'avatar</string>
+    <string name="outdated_backup_file_format">Vous essayez d\'importer un format de fichier de sauvegarde obsolète</string>
+    <string name="audiobook">Livre audio</string>
+    <string name="unified_push_distributor">Distributeur UnifiedPush</string>
+    <string name="restore_warning_continued">Ne tentez pas de restaurer des sauvegardes que vous n\'avez pas créées vous-même !</string>
 </resources>

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

@@ -18,7 +18,7 @@
     <string name="action_unblock_domain">Desbloquear dominio</string>
     <string name="action_block_participant">Bloquear persoa</string>
     <string name="action_unblock_participant">Desbloquear persoa</string>
-    <string name="title_activity_manage_accounts">Xestionar contas</string>
+    <string name="title_activity_manage_accounts">Xestionar Contas</string>
     <string name="title_activity_settings">Axustes</string>
     <string name="title_activity_sharewith">Compartir na conversa</string>
     <string name="title_activity_start_conversation">Iniciar conversa</string>
@@ -354,7 +354,7 @@
     <string name="no_application_found_to_open_file">Non se atopou unha app para abrir o ficheiro</string>
     <string name="no_application_found_to_open_link">Non se atopou app para abrir a ligazón</string>
     <string name="no_application_found_to_view_contact">Non se atopou app para ver o contacto</string>
-    <string name="pref_show_dynamic_tags">Información do estado</string>
+    <string name="pref_show_dynamic_tags">Etiquetas dinámicas</string>
     <string name="pref_show_dynamic_tags_summary">Mostra o estado debaixo do nome do contacto</string>
     <string name="enable_notifications">Habilitar notificacións</string>
     <string name="no_conference_server_found">Non se atopou ningún servidor de conversa en grupo</string>
@@ -907,7 +907,7 @@
     <string name="rtp_state_incoming_call">Chamada entrante</string>
     <string name="rtp_state_incoming_video_call">Videochamada entrante</string>
     <string name="rtp_state_content_add_video">Cambiar a unha chamada de vídeo?</string>
-    <string name="rtp_state_content_add">Engadir pistas adicionais?</string>
+    <string name="rtp_state_content_add">Engadir rutas adicionais\?</string>
     <string name="rtp_state_connecting">Conectando</string>
     <string name="rtp_state_connected">Conectado</string>
     <string name="rtp_state_reconnecting">Reconectando</string>
@@ -1010,4 +1010,8 @@
     <string name="search_group_chats">Buscar conversas en grupo</string>
     <string name="group_chats">Conversas en grupo</string>
     <string name="channel_discover_opt_in_message">O descubrimento de canles usa un servizo externo chamado &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Ao usar esta ferramenta transmitirás o teu enderezo IP e termos de busca a ese servizo. Le a súa &lt;a href=https://search.jabber.network/privacy&gt;Política de Privacidade&lt;/a&gt; para saber máis.</string>
+    <string name="restore_warning_continued">Non intentes restablecer unha copia de apoio que non tiveses creado ti!</string>
+    <string name="outdated_backup_file_format">Estás intentando importar un ficheiro de apoio co formato antigo</string>
+    <string name="audiobook">Audiolibro</string>
+    <string name="reconnect_on_other_host">Volver conectar noutro servidor</string>
 </resources>

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

@@ -254,7 +254,7 @@
     <string name="contact_added_you">Il contatto ti ha aggiunto alla sua lista contatti</string>
     <string name="add_back">Aggiungi anche tu</string>
     <string name="contact_has_read_up_to_this_point">%s ha letto fino a questo punto</string>
-    <string name="contacts_have_read_up_to_this_point">%s ha letto fino a questo punto</string>
+    <string name="contacts_have_read_up_to_this_point">%s hanno letto fino a questo punto</string>
     <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s + altri %2$d hanno letto fino a questo punto</string>
     <string name="everyone_has_read_up_to_this_point">Tutti hanno letto fino a questo punto</string>
     <string name="publish">Pubblica</string>
@@ -1021,4 +1021,8 @@
     <string name="save_as_group_chat">Salva come chat di gruppo</string>
     <string name="search_group_chats">Cerca chat di gruppo</string>
     <string name="channel_discover_opt_in_message">La scoperta dei canali usa un servizio di terze parti chiamato &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;L\'uso di questa funzione invierà il tuo indirizzo IP e i termini di ricerca a quel servizio. Vedi la sua &lt;a href=https://search.jabber.network/privacy&gt;informativa sulla privacy&lt;/a&gt; per maggiori informazioni.</string>
+    <string name="restore_warning_continued">Non tentare di ripristinare dei backup che non hai creato te stesso!</string>
+    <string name="outdated_backup_file_format">Stai tentando di importare un formato di file di backup obsoleto</string>
+    <string name="audiobook">Audiolibro</string>
+    <string name="reconnect_on_other_host">Riconnetti su altro host</string>
 </resources>

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

@@ -1040,4 +1040,8 @@
     <string name="channel_discover_opt_in_message">Wykrywanie kanałów korzysta z usługi innego podmiotu o nazwie &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Użycie tej funkcji spowoduje przesłanie adresu IP i wyszukiwanych terminów do tej usługi. Zobacz ich &lt;a href=https://search.jabber.network/privacy&gt;Politykę prywatności&lt;/a&gt;, aby uzyskać więcej informacji.</string>
     <string name="group_chats">Czaty grupowe</string>
     <string name="save_as_group_chat">Zapisz jako czat grupowy</string>
+    <string name="restore_warning_continued">Nie próbuj przywracać kopii zapasowych, których nie utworzono samodzielnie!</string>
+    <string name="outdated_backup_file_format">Próbujesz zaimportować plik kopii zapasowej o przestarzałym formacie</string>
+    <string name="audiobook">Audiobook</string>
+    <string name="reconnect_on_other_host">Połącz się ponownie na innym hoście</string>
 </resources>

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

@@ -1029,4 +1029,8 @@
     <string name="channel_discover_opt_in_message">Descoperirea de canale publice folosește un serviciu terț numit &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Folosind această funcție se va transmite adresa dumneavoastră IP și cuvintele căutate către acest serviciu. Pentru mai multe informații citiți &lt;a href=https://search.jabber.network/privacy&gt;Politica de confidențialitate&lt;/a&gt; a serviciului.</string>
     <string name="save_as_group_chat">Salvare ca discuție de grup</string>
     <string name="search_group_chats">Caută discuții de grup</string>
+    <string name="restore_warning_continued">Nu încercați să restaurați copii de rezervă pe care nu le-ați creat personal!</string>
+    <string name="outdated_backup_file_format">Încercați să importați un fișier copie de rezervă format vechi</string>
+    <string name="audiobook">Carte audio</string>
+    <string name="reconnect_on_other_host">Reconectat pe altă gazdă</string>
 </resources>

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

@@ -8,7 +8,7 @@
     <string name="action_contact_details">Сведения о контакте</string>
     <string name="action_muc_details">Подробности конференции</string>
     <string name="channel_details">Сведения о канале</string>
-    <string name="action_add_account">Добавить аккаунт</string>
+    <string name="action_add_account">Добавить учётную запись</string>
     <string name="action_edit_contact">Редактировать контакт</string>
     <string name="action_add_phone_book">Добавить в адресную книгу</string>
     <string name="action_delete_contact">Удалить из списка</string>
@@ -53,7 +53,7 @@
     <string name="contact_blocked">Контакт заблокирован</string>
     <string name="blocked">Заблокирован</string>
     <string name="remove_bookmark_text">Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой, будут сохранены.</string>
-    <string name="register_account">Создать новый аккаунт на сервере</string>
+    <string name="register_account">Создать новую учётную запись на сервере</string>
     <string name="change_password_on_server">Изменить пароль на сервере</string>
     <string name="share_with">Поделиться с</string>
     <string name="start_conversation">Начать беседу</string>
@@ -139,7 +139,7 @@
     <string name="accept">Принять</string>
     <string name="error">Произошла ошибка</string>
     <string name="recording_error">Ошибка</string>
-    <string name="your_account">Ваш аккаунт</string>
+    <string name="your_account">Ваша учётная запись</string>
     <string name="send_presence_updates">Отправлять присутствие</string>
     <string name="receive_presence_updates">Получать присутствие</string>
     <string name="ask_for_presence_updates">Запрашивать присутствие</string>
@@ -183,7 +183,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>
@@ -267,7 +267,7 @@
     <string name="private_message_to">отправить %s</string>
     <string name="send_private_message_to">Приватное сообщение %s</string>
     <string name="connect">Подключиться</string>
-    <string name="account_already_exists">Аккаунт уже существует</string>
+    <string name="account_already_exists">Эта учётная запись уже существует</string>
     <string name="next">Далее</string>
     <string name="server_info_session_established">Сеанс установлен</string>
     <string name="skip">Пропустить</string>
@@ -329,7 +329,7 @@
     <string name="notification_backup_created_subtitle">Файлы резервной копии сохранены в %s</string>
     <string name="restoring_backup">Восстановление из резервной копии</string>
     <string name="notification_restored_backup_title">Восстановление из резервной копии выполнено</string>
-    <string name="notification_restored_backup_subtitle">Не забудьте включить аккаунт</string>
+    <string name="notification_restored_backup_subtitle">Не забудьте включить учётную запись.</string>
     <string name="choose_file">Выбрать файл</string>
     <string name="receiving_x_file">%1$s загружается (%2$d%% выполнено)</string>
     <string name="download_x_file">Загрузить %s</string>
@@ -526,17 +526,18 @@
     <string name="battery_optimizations_enabled_dialog">Ваше устройство использует агрессивную оптимизацию энергопотребления %1$s, что может привести к задержке уведомлений и даже потере сообщений.\nСейчас появится предложение её отключить.</string>
     <string name="disable">Запретить</string>
     <string name="selection_too_large">Выбранная область слишком большая</string>
-    <string name="no_accounts">(Нет активных аккаунтов)</string>
+    <string name="no_accounts">(Нет активированных учётных записей)</string>
     <string name="this_field_is_required">Незаполненное поле</string>
     <string name="correct_message">Исправить сообщение</string>
     <string name="send_corrected_message">Отправить исправленное сообщение</string>
     <string name="no_keys_just_confirm">Вы уже подтвердили, что электронный отпечаток принадлежит этому человеку. Выбрав \"Готово\", вы только подтвердите, что %s является участником конференции.</string>
-    <string name="this_account_is_disabled">Вы отключили этот аккаунт</string>
+    <string name="this_account_is_disabled">Вы отключили эту учётную запись</string>
     <string name="security_error_invalid_file_access">Ошибка безопасности: недействительный доступ к файлу</string>
     <string name="no_application_to_share_uri">Не найдено приложения для передачи URI</string>
     <string name="share_uri_with">Отправить URI…</string>
     <string name="agree_and_continue">Согласиться и продолжить</string>
-    <string name="magic_create_text">Мы поможем Вам создать аккаунт на conversations.im¹.\nВыбрав conversations.im в качестве провайдера, вы сможете общаться с пользователями других провайдеров, сообщив им свой полный XMPP-адрес.</string>
+    <string name="magic_create_text">Мы поможем Вам создать аккаунт на conversations.im.
+\nВыбрав conversations.im в качестве провайдера, вы сможете общаться с пользователями других провайдеров, сообщив им свой полный XMPP-адрес.</string>
     <string name="your_full_jid_will_be">Ваш полный XMPP-адрес будет: %s</string>
     <string name="create_account">Создать аккаунт</string>
     <string name="use_own_provider">Использовать свой провайдер</string>
@@ -880,9 +881,9 @@
     <string name="discover_channels">Найти каналы</string>
     <string name="search_channels">Поиск каналов</string>
     <string name="channel_discovery_opt_in_title">Возможно нарушение конфиденциальности!</string>
-    <string name="i_already_have_an_account">У меня уже есть аккаунт</string>
+    <string name="i_already_have_an_account">У меня уже есть учётная запись</string>
     <string name="add_existing_account">Добавить существующий аккаунт</string>
-    <string name="register_new_account">Зарегистрировать новую учетную запись</string>
+    <string name="register_new_account">Создать новую учетную запись</string>
     <string name="this_looks_like_a_domain">Это похоже на имя домена</string>
     <string name="add_anway">Добавить все равно</string>
     <string name="this_looks_like_channel">Это похоже на адрес канала</string>
@@ -927,7 +928,7 @@
     <string name="incoming_call">Входящий вызов</string>
     <string name="missed_call_timestamp">Пропущенный вызов · %s</string>
     <string name="outgoing_call">Исходящий вызов</string>
-    <string name="missed_call">Пропущен вызов</string>
+    <string name="missed_call">Пропущенный вызов</string>
     <string name="audio_call">Аудиовызов</string>
     <string name="video_call">Видеовызов</string>
     <string name="help">Помощь</string>
@@ -975,9 +976,63 @@
     <string name="unable_to_enable_video">Невозможно включить видео.</string>
     <string name="plain_text_document">Текстовые данные</string>
     <plurals name="n_missed_calls">
-        <item quantity="one">%d пропущеный звонок</item>
-        <item quantity="few">%d пропущенных звонков</item>
-        <item quantity="many">%d пропущеныыз звонков</item>
-        <item quantity="other">%d пропущеныыз звонков</item>
+        <item quantity="one">%d пропущенный вызов</item>
+        <item quantity="few">%d пропущенных вызова</item>
+        <item quantity="many">%d пропущенных вызовов</item>
+        <item quantity="other">%d пропущенных вызовов</item>
     </plurals>
+    <string name="account_status_incompatible_client">Несовместимый клиент</string>
+    <string name="group_chats">Групповые беседы</string>
+    <string name="multimedia_file">медиафайл</string>
+    <string name="continue_btn">Продолжить</string>
+    <string name="pref_up_push_server_title">Сервер уведомлений</string>
+    <string name="unified_push_distributor">Распределитель UnifiedPush</string>
+    <string name="missed_calls_channel_name">Пропущенные вызовы</string>
+    <string name="rtp_state_reconnecting">Переподключение</string>
+    <string name="no_xmpp_adddress_found">Адрес XMPP не найден</string>
+    <string name="pref_up_push_account_title">Учётная запись XMPP</string>
+    <string name="conference_technical_problems">Вы покинули эту беседу из-за технических причин</string>
+    <string name="rtp_state_content_add">Добавить дополнительные треки\?</string>
+    <string name="reconnecting_call">Переподключение к звонку</string>
+    <string name="reconnecting_video_call">Переподключение к видеовызову</string>
+    <string name="outgoing_call_timestamp">Исходящий вызов · %s</string>
+    <string name="rtp_state_security_error">Проблема подтверждения</string>
+    <string name="account_status_temporary_auth_failure">Временная ошибка аутентификации</string>
+    <string name="delete_avatar">Удалить аватар</string>
+    <string name="switch_to_video">Переключиться на видео</string>
+    <string name="reject_switch_to_video">Отклонить запрос смены на видео</string>
+    <string name="decline">Отклонить</string>
+    <string name="could_not_delete_account_from_server">Невозможно удалить учётную запись на сервере</string>
+    <string name="delete_from_server">Удалить учётную запись на сервере</string>
+    <string name="save_as_group_chat">Сохранить как групповую беседу</string>
+    <string name="pref_autojoin">Синхронизировать закладки</string>
+    <string name="pref_autojoin_summary">Устанавливать флаг \"автоприсоединение\" при входе в- и выходе из MUC, и реагировать на изменения от других клиентов.</string>
+    <string name="search_group_chats">Поиск по групповым беседам</string>
+    <string name="download_failed_invalid_file">Загрузка провалена: неверный файл</string>
+    <string name="rtp_state_content_add_video">Перейти на видеовызов\?</string>
+    <string name="outgoing_call_duration_timestamp">Исходящий вызов (%s) · %s</string>
+    <string name="incoming_call_duration_timestamp">Входящий вызов (%s) · %s</string>
+    <string name="account_registrations_are_not_supported">Регистрации учётных записей не поддерживаются</string>
+    <string name="audio_video_disabled_tor">Звонки выключены, пока используется Tor</string>
+    <string name="pref_up_push_account_summary">Учётная запись для получения пуш-уведомлений.</string>
+    <string name="pref_up_push_server_summary">Выбираемый пользователем сервер для перенаправления уведомлений на Ваше устройство.</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>
+    <string name="no_account_deactivated">Нет (неактивно)</string>
+    <string name="verifying_omemo_keys_trusted_source_account">Вы подтверждаете ваши собственные OMEMO-ключи. Это безопасно только если вы перешли по ссылке из доверенного источника, где только вы могли разместить эту ссылку.</string>
+    <string name="restore_warning_continued">Не пытайтесь восстановить резервные копии, которые не были созданы вами!</string>
+    <plurals name="n_missed_calls_from_x">
+        <item quantity="one">%1$d пропущенный вызов от %2$s</item>
+        <item quantity="few">%1$d пропущенных вызова от %2$s</item>
+        <item quantity="many">%1$d пропущенных вызовов от %2$s</item>
+        <item quantity="other">%1$d пропущенных вызовов от %2$s</item>
+    </plurals>
+    <string name="reconnect_on_other_host">Переподключиться на другой сервер</string>
+    <string name="outdated_backup_file_format">Вы попытались импортировать резервную копию в устаревшем формате</string>
+    <plurals name="n_missed_calls_from_m_contacts">
+        <item quantity="one">%1$d пропущенный вызов от %2$d контакта</item>
+        <item quantity="few">%1$d пропущенных вызова от %2$d контактов</item>
+        <item quantity="many">%1$d пропущенных вызовов от %2$d контактов</item>
+        <item quantity="other">%1$d пропущенных вызовов от %2$d контактов</item>
+    </plurals>
+    <string name="audiobook">Аудиокнига</string>
 </resources>

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

@@ -53,7 +53,7 @@
     <string name="remove_bookmark_text">%s kişisini yer imlerinden çıkarmak ister misiniz? Bu yer imi ile kayıtlı konuşmalar silinmeyecektir.</string>
     <string name="register_account">Sunucuda yeni bir hesap oluştur</string>
     <string name="change_password_on_server">Sunucudaki şifreni değiştir</string>
-    <string name="share_with">Paylaş...</string>
+    <string name="share_with">Paylaş…</string>
     <string name="start_conversation">Konuşma başlat</string>
     <string name="invite_contact">Kişi davet et</string>
     <string name="invite">Davet et </string>
@@ -73,18 +73,20 @@
     <string name="send_now">Şimdi gönder</string>
     <string name="send_never">Bir daha sorma</string>
     <string name="problem_connecting_to_account">Hesaba bağlanılamadı</string>
-    <string name="problem_connecting_to_accounts">Birden fazla hesaba bağlanılamadı.</string>
-    <string name="touch_to_fix">Hesaplarınızı yönetmek için dokununuz.</string>
+    <string name="problem_connecting_to_accounts">Birden fazla hesaba bağlanılamadı</string>
+    <string name="touch_to_fix">Hesaplarınızı yönetmek için dokununuz</string>
     <string name="attach_file">Dosya ekle</string>
     <string name="not_in_roster">Eksik olan bu kişiyi listenize eklemek ister misiniz?</string>
     <string name="add_contact">Kişi ekle</string>
     <string name="send_failed">ulaştırılamadı</string>
-    <string name="preparing_image">Görüntü gönderilmeye hazırlanılıyor.</string>
-    <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="preparing_image">Görüntü gönderilmeye hazırlanılıyor</string>
+    <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_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>
+    <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>
     <string name="delete_file_dialog">Dosyayı sil</string>
     <string name="delete_file_dialog_msg">Bu dosyayı silmek istediğinizden emin misiniz? \n\n<b>Uyarı:</b> Bu eylem, bu dosyanın diğer aygıt ve sunucularda kayıtlı kopyalarını silmeyecektir.</string>
     <string name="also_end_conversation">Devamında bu konuşmayı kapat</string>
@@ -99,7 +101,7 @@
     <string name="send_unencrypted">Şifrelenmemiş gönder</string>
     <string name="decryption_failed">Deşifre edilemedi. Uygun bir özel anahtarınız olmayabilir.</string>
     <string name="openkeychain_required">OpenKeychain</string>
-    <string name="openkeychain_required_long"><![CDATA[%1$s mesajları şifrelemek, deşifre etmek ve genel anahtarlarınızı yönetmek için <b>OpenKeychain</b> kullanmaktadır. \n\nlt GPLv3+ altında lisanslıdır ve F-Droid ve Google Play\'de mevcuttur. \n\n<br><br><small>(Lütfen devamında %1$s\'ı yeniden başlatın.</small>)]]></string>
+    <string name="openkeychain_required_long">%1$s mesajları şifrelemek, deşifre etmek ve genel anahtarlarınızı yönetmek için &lt;b&gt;OpenKeychain&lt;/b&gt; kullanmaktadır.&lt;br&gt;&lt;br&gt;lt GPLv3+ altında lisanslıdır ve F-Droid ve Google Play\'de mevcuttur.&lt;br&gt;&lt;br&gt;&lt;small&gt;(Lütfen devamında %1$s\'ı yeniden başlatın.)&lt;/small&gt;</string>
     <string name="restart">Yeniden başlat</string>
     <string name="install">Kur</string>
     <string name="openkeychain_not_installed">Lütfen OpenKeychain’i kur</string>
@@ -126,7 +128,7 @@
     <string name="pref_notification_grace_period_summary">Cihazlarınızın birinde faaliyet tespit edilmesinden sonra  zaman hatırlatmalarının susturulma uzunluğu.</string>
     <string name="pref_advanced_options">Gelişmiş</string>
     <string name="pref_never_send_crash">Asla çöküş raporu gönderme</string>
-    <string name="pref_never_send_crash_summary">Yığın izi göndererek gelişime yardımcı oluyorsunuz.</string>
+    <string name="pref_never_send_crash_summary">Yığın izi göndererek gelişime yardımcı oluyorsunuz</string>
     <string name="pref_confirm_messages">İletileri onayla</string>
     <string name="pref_confirm_messages_summary">Onların iletilerini aldığınızda ve okuduğunuzda, kişilerinizin bunu bilmesini sağlayın</string>
     <string name="pref_prevent_screenshots">Ekran görüntülerini engelle</string>
@@ -145,10 +147,12 @@
     <string name="attach_take_picture">Resim çek</string>
     <string name="preemptively_grant">Abonelik isteğini peşinen kabul et</string>
     <string name="error_not_an_image_file">Seçtiğiniz dosya bir görüntü dosyası değil</string>
-    <string name="error_compressing_image">Görüntü dosyası dönüştürülemedi.</string>
+    <string name="error_compressing_image">Görüntü dosyası dönüştürülemedi</string>
     <string name="error_file_not_found">Dosya bulunamadı</string>
     <string name="error_io_exception">Genel G/Ç hatası. Depolama yeri kalmamış olabilir mi?</string>
-    <string name="error_security_exception_during_image_copy">Bu görüntüyü seçmekte kullandığınız uygulama, dosyanın okunması için yeterli izinleri sağlayamadı. Görüntüyü seçmek için farklı bir dosya yöneticisi kullan.</string>
+    <string name="error_security_exception_during_image_copy">Bu görüntüyü seçmekte kullandığınız uygulama, dosyanın okunması için yeterli izinleri sağlayamadı.
+\n
+\n<small>Görüntüyü seçmek için farklı bir dosya yöneticisi kullan</small>.</string>
     <string name="error_security_exception">Bu dosyayı paylaşmakta kullandığınız uygulama yeterince yetki sağlamamaktadır. </string>
     <string name="account_status_unknown">Bilinmeyen</string>
     <string name="account_status_disabled">Geçici olarak devre dışı</string>
@@ -161,7 +165,7 @@
     <string name="account_status_regis_fail">Hesap oluşturulamadı</string>
     <string name="account_status_regis_conflict">Kullanıcı adı kullanılıyor</string>
     <string name="account_status_regis_success">Hesap oluşturuldu</string>
-    <string name="account_status_regis_not_sup">Hesap, sunucu tarafından desteklenmiyor.</string>
+    <string name="account_status_regis_not_sup">Hesap, sunucu tarafından desteklenmiyor</string>
     <string name="account_status_regis_invalid_token">Geçersiz hesak simgesi</string>
     <string name="account_status_tls_error">TLS uzlaşması başarısız</string>
     <string name="account_status_tls_error_domain">Alan adı doğrulanamıyor</string>
@@ -182,14 +186,14 @@
     <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ın silinmesi bütün konuşma geçmişinizi siler</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="attach_record_voice">Ses kaydet</string>
     <string name="account_settings_jabber_id">XMPP adresi</string>
     <string name="block_jabber_id">XMPP adresini engelle</string>
     <string name="account_settings_example_jabber_id">kullanıcıadı@ornek.com</string>
     <string name="password">parola</string>
     <string name="invalid_jid">Bu geçerli bir XMPP adresi değil</string>
-    <string name="error_out_of_memory">Yetersiz bellek. Görüntü çok büyük.</string>
+    <string name="error_out_of_memory">Yetersiz bellek. Görüntü çok büyük</string>
     <string name="add_phone_book_text">%s kişisini listenize eklemek ister misiniz?</string>
     <string name="server_info_show_more">Sunucu bilgisi</string>
     <string name="server_info_mam">XEP-0313: MAM</string>
@@ -241,12 +245,14 @@
     <string name="destroy_room">Grup konuşmasını yoket</string>
     <string name="destroy_channel">Kanalı yoket</string>
     <string name="destroy_room_dialog">Bu grup konuşmasını yoketmek istediğinizden emin misiniz?\n\n<b>Uyarı:</b> Grup konuşması sunucudan tamamen kaldırlacaktır.</string>
-    <string name="destroy_channel_dialog">Bu genel kanalı yoketmek istediğnizden emin misiniz?\n\n<b>Uyarı:</b> Kanal sunucudan tamamen kaldırılacaktır. </string>
+    <string name="destroy_channel_dialog">Bu genel kanalı yoketmek istediğnizden emin misiniz\?
+\n
+\n<b>Uyarı:</b> Kanal sunucudan tamamen kaldırılacaktır.</string>
     <string name="could_not_destroy_room">Grup konuşması yokedilemedi</string>
     <string name="could_not_destroy_channel">Kanal yokedilemedi</string>
     <string name="action_edit_subject">Grup konuşma konusunu düzenle</string>
     <string name="topic">Başlık</string>
-    <string name="joining_conference">Grup konuşmasına bağlanılıyor...</string>
+    <string name="joining_conference">Grup konuşmasına bağlanılıyor…</string>
     <string name="leave">Ayrıl</string>
     <string name="contact_added_you">Kişi sizi listesine ekledi</string>
     <string name="add_back">Siz de ekleyin</string>
@@ -261,7 +267,7 @@
     <string name="error_publish_avatar_converting">Resminiz dönüştürülemedi</string>
     <string name="error_saving_avatar">vatar diske kaydedilemedi</string>
     <string name="or_long_press_for_default">(Veya varsayılan değerlere dönmek için uzun süre basılı tutun)</string>
-    <string name="error_publish_avatar_no_server_support">Sunucunuz avatarların tanıtılmasını desteklemiyor.</string>
+    <string name="error_publish_avatar_no_server_support">Sunucunuz avatarların tanıtılmasını desteklemiyor</string>
     <string name="private_message">fısıldandı</string>
     <string name="private_message_to">%s kişisine</string>
     <string name="send_private_message_to">%s kişisine özel ileti gönder</string>
@@ -274,7 +280,9 @@
     <string name="enable">Etkinleştir</string>
     <string name="conference_requires_password">Grup konuşması şifre talep ediyor</string>
     <string name="enter_password">Parolayı gir</string>
-    <string name="request_presence_updates">Lütfen bağlantınızdan önce çevrim içi durum bildirimi talep edin. \n\n<small>Bu, bağlantınızın hangi konuşma uygulamasını kullandığını belirlemekte kullanılacak</small>. </string>
+    <string name="request_presence_updates">Lütfen bağlantınızdan önce çevrim içi durum bildirimi talep edin.
+\n
+\n<small>Bu, bağlantınızın hangi konuşma uygulamasını kullandığını belirlemekte kullanılacak</small>.</string>
     <string name="request_now">Şimdi iste</string>
     <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>
@@ -330,7 +338,7 @@
     <string name="notification_backup_created_subtitle">Yedekleme dosyaları %s\'da depolandı</string>
     <string name="restoring_backup">Yedekleme yükleniyor</string>
     <string name="notification_restored_backup_title">Yedekleminiz yüklendi</string>
-    <string name="notification_restored_backup_subtitle">Hesabı etkinleştirmeyi unutmayın</string>
+    <string name="notification_restored_backup_subtitle">Hesabı etkinleştirmeyi unutmayın.</string>
     <string name="choose_file">Dosya seç</string>
     <string name="receiving_x_file">%1$s alıyor/(%2$d%% tamamlandı)</string>
     <string name="download_x_file">%s indir</string>
@@ -358,7 +366,8 @@
     <string name="clear_other_devices">Aygıtları sil</string>
     <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.\İkinizin de çevrim içi durum aboneliği oldudğundan emin olun.</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>
     <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>
@@ -480,7 +489,7 @@
     <string name="unable_to_parse_certificate">Sertifika çözümlenemedi</string>
     <string name="mam_prefs">Arşivleme tercihleri</string>
     <string name="server_side_mam_prefs">Sunucu tarafı arşivleme tercihleri</string>
-    <string name="fetching_mam_prefs">Arşivleme tercihleri alınıyor. Lütfen bekleyin...</string>
+    <string name="fetching_mam_prefs">Arşivleme tercihleri alınıyor. Lütfen bekleyin…</string>
     <string name="unable_to_fetch_mam_prefs">Arşivleme tercihleri alınamadı</string>
     <string name="captcha_required">CAPTCHA gerekli</string>
     <string name="captcha_hint">Resimdeki yazıyı girin</string>
@@ -511,7 +520,10 @@
     <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. Böylelikle, tüm rehberinizindeki kişilerin tam adları ve avatarlarını görebileceksiniz. \n\n%1$s kişilerinizi sunucunuza yüklemeyecek olup, sadece cihazınız üzerinden eşleştirme yapacaktır.</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.
+\n
+\n%1$s kişilerinizi sunucunuza yüklemeyecek olup, sadece cihazınız üzerinden eşleştirme yapacaktır.</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>
@@ -533,7 +545,7 @@
     <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="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="your_full_jid_will_be">Tam XMPP adresiniz %s olacak</string>
@@ -553,7 +565,7 @@
     <string name="registration_please_wait">Hesap oluşturulamadı: Sonra yeniden deneyin</string>
     <string name="registration_password_too_weak">Kayıt Başarısız: Parola çok zayıf</string>
     <string name="choose_participants">Katılımcıları seç</string>
-    <string name="creating_conference">Grup konuşması oluşturuluyor...</string>
+    <string name="creating_conference">Grup konuşması oluşturuluyor…</string>
     <string name="invite_again">Yeniden davet et</string>
     <string name="gp_disable">Devre dışı bırak</string>
     <string name="gp_short">Kısa</string>
@@ -621,7 +633,8 @@
     <string name="show_inactive_devices">Aktif olmayanları göster</string>
     <string name="hide_inactive_devices">Aktif olmayanları sakla</string>
     <string name="distrust_omemo_key">Güvensiz aygıt</string>
-    <string name="distrust_omemo_key_text">Bu cihazın doğrulamasını kaldırmak istediğinizden emin misiniz\? Bu cihaz ve cihazdan gelen mesajlar güvenilmez olarak işaretlenecektir.</string>
+    <string name="distrust_omemo_key_text">Bu cihazın doğrulamasını kaldırmak istediğinizden emin misiniz\?
+\nBu cihaz ve cihazdan gelen mesajlar güvenilmez olarak işaretlenecektir.</string>
     <plurals name="seconds">
         <item quantity="one">%d saniye</item>
         <item quantity="other">%d saniye</item>
@@ -651,7 +664,7 @@
     <string name="encrypting_message">İletiyi şifrelemek</string>
     <string name="not_fetching_history_retention_period">Yerel saklama süresi nedeniyle ileti getirilmiyor.</string>
     <string name="transcoding_video">Video sıkıştırılıyor</string>
-    <string name="corresponding_conversations_closed">Konuşma sonlandı</string>
+    <string name="corresponding_conversations_closed">Konuşma sonlandı.</string>
     <string name="contact_blocked_past_tense">Kişi engellendi.</string>
     <string name="pref_notifications_from_strangers">Yabancılardan bildirimler</string>
     <string name="pref_notifications_from_strangers_summary">Yabancılardan alınan ileti ve aramaları bildir.</string>
@@ -712,8 +725,8 @@
     <string name="small">Küçük</string>
     <string name="medium">Orta</string>
     <string name="large">Büyük</string>
-    <string name="not_encrypted_for_this_device">İleti bu cihaz için şifrelenmedi</string>
-    <string name="omemo_decryption_failed">OMEMO mesajı çözümlenemedi</string>
+    <string name="not_encrypted_for_this_device">İleti bu cihaz için şifrelenmedi.</string>
+    <string name="omemo_decryption_failed">OMEMO mesajı çözümlenemedi.</string>
     <string name="undo">geri al</string>
     <string name="location_disabled">Konum paylaşımı devre dışı bırakıldı</string>
     <string name="action_fix_to_location">Konumu sabitle</string>
@@ -725,7 +738,7 @@
     <string name="title_activity_show_location">Konumu göster</string>
     <string name="share">Paylaş</string>
     <string name="unable_to_start_recording">Kayıt başlatılamadı</string>
-    <string name="please_wait">Lütfen bekleyin...</string>
+    <string name="please_wait">Lütfen bekleyin…</string>
     <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>
@@ -751,7 +764,7 @@
     <string name="foreground_service_channel_description">Bu bildirim kategorisi %1$s\'ın çalıştığını sürekli belirtmekte kullanmaktadır.</string>
     <string name="notification_group_status_information">Durum bilgisi</string>
     <string name="error_channel_name">Bağlantı Sorunları</string>
-    <string name="error_channel_description">Bu bildirim kategorisi, bir hesaba bağlanmakta sorun olduğunu belirtmekte kullanılır</string>
+    <string name="error_channel_description">Bu bildirim kategorisi, bir hesaba bağlanmakta sorun olduğunu belirtmekte kullanılır.</string>
     <string name="notification_group_messages">İletiler</string>
     <string name="notification_group_calls">Aramalar</string>
     <string name="messages_channel_name">İletiler</string>
@@ -759,7 +772,7 @@
     <string name="ongoing_calls_channel_name">Yapılan aramalar</string>
     <string name="missed_calls_channel_name">Cevapsız aramalar</string>
     <string name="silent_messages_channel_name">Sessiz iletiler</string>
-    <string name="silent_messages_channel_description">Bu bildirim grubu, bildirimlerin herhangi bir ses çıkarmaması gerektiğini belirtmekte kullanılır. Mesela başka bir cihazda aktif olunduğunda (Mühlet)</string>
+    <string name="silent_messages_channel_description">Bu bildirim grubu, bildirimlerin herhangi bir ses çıkarmaması gerektiğini belirtmekte kullanılır. Mesela başka bir cihazda aktif olunduğunda (Mühlet).</string>
     <string name="delivery_failed_channel_name">Başarısız gönderiler</string>
     <string name="pref_message_notification_settings">İleti bildirim ayarları</string>
     <string name="pref_incoming_call_notification_settings">Gelen arama bildirimleri ayarları</string>
@@ -768,13 +781,13 @@
     <string name="view_media">Medyayı görüntüle</string>
     <string name="group_chat_members">Katılımcılar</string>
     <string name="media_browser">Medya tarayıcısı</string>
-    <string name="security_violation_not_attaching_file">Dosya güvenlik ihlalinden dolayı dahil edilmedi</string>
+    <string name="security_violation_not_attaching_file">Dosya güvenlik ihlalinden dolayı dahil edilmedi.</string>
     <string name="pref_video_compression">Video kalitesi</string>
     <string name="pref_video_compression_summary">Daha düşük kalite, daha ufak dosya anlamına gelir</string>
     <string name="video_360p">Orta (360P)</string>
     <string name="video_720p">Yüksek (720p)</string>
     <string name="cancelled">iptal edildi</string>
-    <string name="already_drafting_message">Zaten taslak halinde bir iletiniz var</string>
+    <string name="already_drafting_message">Zaten taslak halinde bir iletiniz var.</string>
     <string name="feature_not_implemented">Özellik uygulanmadı </string>
     <string name="invalid_country_code">Geçersiz ülke kodu</string>
     <string name="choose_a_country">Bir ülke seçin</string>
@@ -793,25 +806,25 @@
     <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="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="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>
-    <string name="verifying">Doğrulanıyor...</string>
-    <string name="requesting_sms">SMS talep ediliyor...</string>
-    <string name="incorrect_pin">Girdiğiniz kod yanlış</string>
-    <string name="pin_expired">Girdiğiniz kodun tarihi geçmiş</string>
-    <string name="unknown_api_error_network">Bilinmeyen ağ hatası</string>
-    <string name="unknown_api_error_response">Sunucudan bilinmeyen cevap</string>
-    <string name="unable_to_connect_to_server">Sunucuya bağlanılamadı</string>
-    <string name="unable_to_establish_secure_connection">Güvenli bağlantı oluşturulamadı</string>
-    <string name="unable_to_find_server">Sunucu bulunamadı</string>
-    <string name="something_went_wrong_processing_your_request">Talebinizin işlenmesinde bir hata meydana geldi</string>
+    <string name="verifying">Doğrulanıyor…</string>
+    <string name="requesting_sms">SMS talep ediliyor…</string>
+    <string name="incorrect_pin">Girdiğiniz kod yanlış.</string>
+    <string name="pin_expired">Girdiğiniz kodun tarihi geçmiş.</string>
+    <string name="unknown_api_error_network">Bilinmeyen ağ hatası.</string>
+    <string name="unknown_api_error_response">Sunucudan bilinmeyen cevap.</string>
+    <string name="unable_to_connect_to_server">Sunucuya bağlanılamadı.</string>
+    <string name="unable_to_establish_secure_connection">Güvenli bağlantı oluşturulamadı.</string>
+    <string name="unable_to_find_server">Sunucu bulunamadı.</string>
+    <string name="something_went_wrong_processing_your_request">Talebinizin işlenmesinde bir hata meydana geldi.</string>
     <string name="invalid_user_input">Geçersiz kullanıcı girişi</string>
     <string name="temporarily_unavailable">Geçici olarak ulaşılamıyor. Daha sonra tekrar deneyiniz.</string>
-    <string name="no_network_connection">Ağ bağlantısı yok</string>
-    <string name="try_again_in_x">Lütfen %s içerisinde tekrar deneyiniz.</string>
+    <string name="no_network_connection">Ağ bağlantısı yok.</string>
+    <string name="try_again_in_x">Lütfen %s içerisinde tekrar deneyiniz</string>
     <string name="rate_limited">Sınırlandınız</string>
     <string name="too_many_attempts">Çok fazla girişim</string>
     <string name="the_app_is_out_of_date">Bu uygulamamının tarihi geçmiş bir versiyonunu kullanıyorsunuz.</string>
@@ -820,22 +833,22 @@
     <string name="enter_your_name_instructions">Adres defterlerinde sizin kim olduğunuzu bilmeyen kişilerin sizi tanıması için lütfen isminizi girin.</string>
     <string name="your_name">Adınız</string>
     <string name="enter_your_name">Adınızı girin</string>
-    <string name="no_name_set_instructions">Adınızı belirlemek için düzenle tuşunu kullanın</string>
+    <string name="no_name_set_instructions">Adınızı belirlemek için düzenle tuşunu kullanın.</string>
     <string name="reject_request">Talebi reddet</string>
     <string name="install_orbot">Orbot\'u yükle</string>
     <string name="start_orbot">Orbot\'u başlat</string>
-    <string name="no_market_app_installed">Herhangi bir mağaza uygulaması yüklenmedi</string>
-    <string name="group_chat_will_make_your_jabber_id_public">Bu kanal XMPP adresinizi herkese açık hale getirecek.</string>
+    <string name="no_market_app_installed">Herhangi bir mağaza uygulaması yüklenmedi.</string>
+    <string name="group_chat_will_make_your_jabber_id_public">Bu kanal XMPP adresinizi herkese açık hale getirecek</string>
     <string name="ebook">e-kitap</string>
     <string name="video_original">Orijinal (sıkıştırılmamış)</string>
-    <string name="open_with">Şununla aç...</string>
+    <string name="open_with">Şununla aç…</string>
     <string name="set_profile_picture">Conversations profil resmi</string>
     <string name="choose_account">Hesap seç</string>
     <string name="restore_backup">Yedekleri yükle</string>
     <string name="restore">Geri getir</string>
     <string name="enter_password_to_restore"> Yedekleri geri getirmek için %s hesabının şifrenizi girin.</string>
     <string name="restore_warning">Herhangi bir yüklemeyi klonlama (aynı anda çalışan) girişiminde yedekleri geri yükleme özelliğini kullanmayın. Yedeklerin geri yüklenmesi sadece hesap aktarımları veya asıl cihazı kaybetmeniz durumu için kullanılmalıdır.</string>
-    <string name="unable_to_restore_backup">Yedekler geri yüklenemedi</string>
+    <string name="unable_to_restore_backup">Yedekler geri yüklenemedi.</string>
     <string name="unable_to_decrypt_backup">Yedekleme çözülemedi. Şifre doğru mu?</string>
     <string name="backup_channel_name">Yedekleme &amp; Geri yükle</string>
     <string name="enter_jabber_id">XMPP adresini girin</string>
@@ -848,15 +861,15 @@
     <string name="please_enter_name">Lütfen kanal için bir isim belirleyin</string>
     <string name="please_enter_xmpp_address">Lütfen bir XMPP adresi sağlayın</string>
     <string name="this_is_an_xmpp_address">Bu bir XMPP adresi. Lütfen bir isim belirleyin.</string>
-    <string name="creating_channel">Ortak kanal oluşturuluyor...</string>
+    <string name="creating_channel">Ortak kanal oluşturuluyor…</string>
     <string name="channel_already_exists">Bu kanal zaten var</string>
     <string name="joined_an_existing_channel">Varolan bir kanala katıldınız</string>
     <string name="unable_to_set_channel_configuration">Kanal ayarları kaydedilemedi</string>
     <string name="allow_participants_to_edit_subject">Herhangi birinin başlığı düzenlemesine izin ver</string>
     <string name="allow_participants_to_invite_others">Herhangi birinin davet etmesine izin ver</string>
     <string name="anyone_can_edit_subject">Herhangi biri başlığı düzenleyebilir.</string>
-    <string name="owners_can_edit_subject">Yöneticiler başlığı düzenleyebilir</string>
-    <string name="admins_can_edit_subject">Yöneticiler başlığı düzenleyebilir</string>
+    <string name="owners_can_edit_subject">Yöneticiler başlığı düzenleyebilir.</string>
+    <string name="admins_can_edit_subject">Yöneticiler başlığı düzenleyebilir.</string>
     <string name="owners_can_invite_others">Yöneticiler başkalarını davet edebilir.</string>
     <string name="anyone_can_invite_others">Herhangi biri başkalarını davet edebilir.</string>
     <string name="jabber_ids_are_visible_to_admins">XMPP adresleri yöneticelere görünür.</string>
@@ -884,8 +897,8 @@
     <string name="account_already_setup">Bu hesap zaten kurulu vaziyette</string>
     <string name="please_enter_password">Lütfen bu hesabın şifresini girin</string>
     <string name="unable_to_perform_this_action">Eylem gerçekleştirilemedi</string>
-    <string name="open_join_dialog">Ortak kanala katılınıyor</string>
-    <string name="sharing_application_not_grant_permission">Paylaşımda bulunan uygulama dosya erişimi için yetki sağlamıyor</string>
+    <string name="open_join_dialog">Ortak kanala katılınıyor…</string>
+    <string name="sharing_application_not_grant_permission">Paylaşımda bulunan uygulama dosya erişimi için yetki sağlamıyor.</string>
     <string name="group_chats_and_channels"><![CDATA[Grup Konuşmaları & Kanallar]]></string>
     <string name="jabber_network">jabber.network</string>
     <string name="local_server">Yerel sunucu</string>
@@ -974,11 +987,14 @@
     <string name="server_does_not_support_easy_onboarding_invites">Sunucu, davet oluşturulmasını desteklemiyor</string>
     <string name="no_active_accounts_support_this">Bu özelliği destekleyen aktif bir hesap yok</string>
     <string name="backup_started_message">Yedekleme başlatıldı. Tamamlandığı zaman bir bildirim alacaksınız.</string>
-    <string name="unable_to_enable_video">Video etkinleştirilemedi</string>
+    <string name="unable_to_enable_video">Video etkinleştirilemedi.</string>
     <string name="plain_text_document">Düz metin dosyası</string>
-    <string name="account_registrations_are_not_supported">Hesap kayıtları desteklenmemektedir.</string>
+    <string name="account_registrations_are_not_supported">Hesap kayıtları desteklenmemektedir</string>
     <string name="no_xmpp_adddress_found">Herhangi bir XMPP adresi bulunamadı</string>
     <string name="account_status_temporary_auth_failure">Geçici doğrulama hatası</string>
     <string name="delete_avatar">Avatar\'ı sil</string>
     <string name="audio_video_disabled_tor">Tor kullanırken çağrılar devre dışı</string>
+    <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>
 </resources>

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

@@ -10,7 +10,7 @@
     <string name="action_add_account">Додати обліковий запис</string>
     <string name="action_edit_contact">Редагувати ім\'я</string>
     <string name="action_add_phone_book">Додати до контактів</string>
-    <string name="action_delete_contact">Вилучити зі списку розмов</string>
+    <string name="action_delete_contact">Вилучити зі списку контактів</string>
     <string name="action_block_contact">Заблокувати контакт</string>
     <string name="action_unblock_contact">Розблокувати контакт</string>
     <string name="action_block_domain">Заблокувати домен</string>
@@ -38,17 +38,17 @@
     <string name="moderator">Модератор</string>
     <string name="participant">Учасник</string>
     <string name="visitor">Гість</string>
-    <string name="remove_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="remove_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="contact_blocked">Контакт заблоковано</string>
     <string name="blocked">Заблоковано</string>
-    <string name="remove_bookmark_text">Бажаєте вилучити %s із закладок? Розмову з цією закладкою не буде вилучено.</string>
-    <string name="register_account">Зареєструвати новий обліковий запис</string>
+    <string name="remove_bookmark_text">Бажаєте вилучити %s із закладок\? Розмови, пов\'язані з цією закладкою, залишаться.</string>
+    <string name="register_account">Зареєструвати новий обліковий запис на сервері</string>
     <string name="change_password_on_server">Змінити пароль</string>
-    <string name="share_with">Поділитися</string>
+    <string name="share_with">Поділитися…</string>
     <string name="start_conversation">Почати розмову</string>
     <string name="invite_contact">Запросити до групи</string>
     <string name="invite">Запросити</string>
@@ -65,42 +65,50 @@
     <string name="ok">Гаразд</string>
     <string name="send_now">Надіслати зараз</string>
     <string name="send_never">Ніколи не питати знову</string>
-    <string name="problem_connecting_to_account">Неможливо з\'єднатися з обліковим записом</string>
-    <string name="problem_connecting_to_accounts">Не можу увімкнути режим багатьох облікових записів</string>
-    <string name="touch_to_fix">Перейти для керування обліковими записами</string>
+    <string name="problem_connecting_to_account">Не вдалося з\'єднатися з обліковим записом</string>
+    <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>
-    <string name="preparing_images">Готовий до надсилання зображень</string>
+    <string name="send_failed">не вдалося надіслати</string>
+    <string name="preparing_image">Підготовка до передачі зображення</string>
+    <string name="preparing_images">Підготовка до передачі зображень</string>
     <string name="sharing_files_please_wait">Поширюю файли. Зачекайте, будь ласка…</string>
     <string name="action_clear_history">Стерти історію</string>
     <string name="clear_conversation_history">Стерти історію розмов</string>
-    <string name="clear_histor_msg">Дійсно вилучити усі повідомлення цієї розмови?\n\n<b>Увага:</b> Повідомлення, які було раніше надіслано, й далі залишатимуться на інших пристроях або серверах.</string>
+    <string name="clear_histor_msg">Дійсно вилучити всі повідомлення цієї розмови\?
+\n
+\n<b>Увага:</b> Повідомлення, збережені на інших пристроях або серверах, і надалі залишаться там.</string>
     <string name="delete_file_dialog">Вилучити файл</string>
-    <string name="delete_file_dialog_msg">Ви певні, що бажаєте вилучити цей файл?\n\n<b>Увага:</b> Це не вилучить копії цього файлу, які зберігаються на інших пристроях чи серверах.</string>
+    <string name="delete_file_dialog_msg">Ви впевнені, що хочете вилучити цей файл\?
+\n
+\n<b>Увага:</b> Це не вилучить копії цього файлу, які зберігаються на інших пристроях чи серверах. </string>
     <string name="also_end_conversation">Завершити цю розмову пізніше</string>
     <string name="choose_presence">Вибрати пристрій</string>
     <string name="send_unencrypted_message">Незашифроване повідомлення</string>
     <string name="send_message">Надіслати повідомлення</string>
-    <string name="send_message_to_x">Повідомлення до %s</string>
-    <string name="send_omemo_message">Повідомлення зашифроване OMEMO</string>
-    <string name="send_omemo_x509_message">Повідомлення зашифроване v\\OMEMO</string>
+    <string name="send_message_to_x">Повідомлення %s</string>
+    <string name="send_omemo_message">Повідомлення, зашифроване OMEMO</string>
+    <string name="send_omemo_x509_message">Повідомлення, зашифроване v\\OMEMO</string>
     <string name="send_pgp_message">Повідомлення зашифроване OpenPGP</string>
     <string name="your_nick_has_been_changed">Ваше прізвисько змінено</string>
     <string name="send_unencrypted">Надіслати без шифрування</string>
-    <string name="decryption_failed">Не вдалося розшифрувати. Можливо, у вас відсутній потрібний приватний ключ.</string>
+    <string name="decryption_failed">Не вдалося розшифрувати. Можливо, у Вас відсутній потрібний приватний ключ.</string>
     <string name="openkeychain_required">OpenKeychain</string>
     <string name="restart">Перезапустити</string>
     <string name="install">Встановити</string>
     <string name="openkeychain_not_installed">Будь ласка, встановіть OpenKeychain</string>
     <string name="offering">пропоную…</string>
     <string name="waiting">чекаю…</string>
-    <string name="no_pgp_key">Не знайдено жодного OpenPGP ключа</string>
-    <string name="contact_has_no_pgp_key">Не вдалося зашифрувати повідомлення, оскільки контакт не повідомляє власний публічний ключ.\n\n<small>Будь ласка, запропонуйте вашому співрозмовникові встановити OpenPGP.</small></string>
-    <string name="no_pgp_keys">Не знайдено жодного ключа OpenPGP</string>
-    <string name="contacts_have_no_pgp_keys">Не вдалося розшифрувати повідомлення, оскільки контакт не повідомляє свого публічного ключа.\n\n<small>Будь ласка, попросіть контакт налаштувати OpenPGP.</small></string>
+    <string name="no_pgp_key">Не знайдено ключа OpenPGP</string>
+    <string name="contact_has_no_pgp_key">Не вдалося зашифрувати повідомлення, оскільки контакт не повідомляє свій публічний ключ.
+\n
+\n<small>Будь ласка, попросіть співрозмовника налаштувати OpenPGP.</small></string>
+    <string name="no_pgp_keys">Не знайдено ключів OpenPGP</string>
+    <string name="contacts_have_no_pgp_keys">Не вдалося зашифрувати повідомлення, оскільки контакти не повідомляють свої публічні ключі.
+\n
+\n<small>Будь ласка, попросіть співрозмовників налаштувати OpenPGP.</small></string>
     <string name="pref_general">Загальне</string>
     <string name="pref_accept_files">Приймати файли</string>
     <string name="pref_accept_files_summary">Автоматично приймати файли менші за…</string>
@@ -114,30 +122,32 @@
     <string name="pref_notification_sound">Звук сповіщень</string>
     <string name="pref_notification_sound_summary">Звук сповіщень для нових повідомлень</string>
     <string name="pref_notification_grace_period">Час до блокування</string>
-    <string name="pref_notification_grace_period_summary">Час надсилання сповіщень не буде враховано у разі активності користувача на іншому пристрої</string>
+    <string name="pref_notification_grace_period_summary">Час, протягом якого сповіщення вимкнені після виявлення активності на іншому пристрої.</string>
     <string name="pref_advanced_options">Розширені</string>
     <string name="pref_never_send_crash">Не надсилати звіти про збої</string>
-    <string name="pref_never_send_crash_summary">Надсиланням звітів про відмову ви допомагаєте у розробці</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_ui_options">Інтерфейс користувача</string>
     <string name="openpgp_error">Програма OpenKeychain повідомила про помилку.</string>
-    <string name="bad_key_for_encryption">Неприйнятний ключ для шифрування</string>
+    <string name="bad_key_for_encryption">Неприйнятний ключ для шифрування.</string>
     <string name="accept">Прийняти</string>
     <string name="error">Сталася помилка</string>
     <string name="recording_error">Помилка</string>
     <string name="your_account">Ваш обліковий запис</string>
-    <string name="send_presence_updates">Повідомляти про мою доступність</string>
-    <string name="receive_presence_updates">Отримувати оновлення про доступність</string>
-    <string name="ask_for_presence_updates">Надіслати запит на оновлення доступності</string>
-    <string name="attach_choose_picture">Зображення</string>
+    <string name="send_presence_updates">Повідомляти про присутність</string>
+    <string name="receive_presence_updates">Отримувати оновлення присутності</string>
+    <string name="ask_for_presence_updates">Надіслати запит на оновлення присутності</string>
+    <string name="attach_choose_picture">Обрати зображення</string>
     <string name="attach_take_picture">Зняти світлину</string>
     <string name="preemptively_grant">Попередньо давати запит на підписку</string>
-    <string name="error_not_an_image_file">Переданий файл не є зображенням</string>
-    <string name="error_compressing_image">Помилка при конвертації зображення</string>
+    <string name="error_not_an_image_file">Обраний файл не є зображенням</string>
+    <string name="error_compressing_image">Не вдалося конвертувати зображення</string>
     <string name="error_file_not_found">Файл не знайдено</string>
-    <string name="error_io_exception">Загальна помилка вводу-виводу. Можливо, на вашому пристрої закінчилась пам\'ять для збереження?</string>
-    <string name="error_security_exception_during_image_copy">Застосунок для роботи із зображенням не надав  достатніх дозволів.\n\n<small>Скористайтеся іншим файловим менеджером для вибору зображення</small></string>
+    <string name="error_io_exception">Загальна помилка вводу-виводу. Можливо, на пристрої закінчилася вільна пам\'ять\?</string>
+    <string name="error_security_exception_during_image_copy">Застосунок для роботи із зображенням не надав достатніх дозволів.
+\n
+\n<small>Скористайтеся іншим файловим менеджером для вибору зображення</small>.</string>
     <string name="account_status_unknown">Невідомо</string>
     <string name="account_status_disabled">Тимчасово вимкнено</string>
     <string name="account_status_online">У мережі</string>
@@ -165,27 +175,28 @@
     <string name="mgmt_account_publish_avatar">Опублікувати піктограму користувача</string>
     <string name="mgmt_account_publish_pgp">Опублікувати публічний ключ OpenPGP</string>
     <string name="unpublish_pgp">Вилучити публічний ключ OpenPGP</string>
-    <string name="unpublish_pgp_message">Ви впевнені, що хочете вилучити ваш публічний ключ OpenPGP з вашого оголошення про присутність?\nВаші контакти більше не зможуть надсилати вам повідомлення, зашифровані OpenPGP.</string>
+    <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>
     <string name="account_settings_example_jabber_id">username@example.com</string>
     <string name="password">Пароль</string>
     <string name="invalid_jid">Це недійсна адреса XMPP</string>
-    <string name="error_out_of_memory">Пам\'ять вичерпано. Завелике зображення.</string>
+    <string name="error_out_of_memory">Пам\'ять вичерпано. Завелике зображення</string>
     <string name="add_phone_book_text">Бажаєте додати %s до своєї книги контактів?</string>
     <string name="server_info_show_more">Інформація про сервер</string>
     <string name="server_info_mam">XEP-0313: Керування архівом</string>
     <string name="server_info_carbon_messages">XEP-0280: Копії повідомлень</string>
     <string name="server_info_csi">XEP-0352: Індикація стану клієнта</string>
     <string name="server_info_blocking">XEP-0191: Команди блокування</string>
-    <string name="server_info_roster_version">XEP-0237: Зміни у списку розмов</string>
+    <string name="server_info_roster_version">XEP-0237: Зміни у списку контактів</string>
     <string name="server_info_stream_management">XEP-0198: Керування потоком</string>
     <string name="server_info_external_service_discovery">XEP-0215: Виявлення зовнішньої служби</string>
-    <string name="server_info_pep">XEP-0163: Персональне (піктограми користувачів, OMEMO)</string>
+    <string name="server_info_pep">XEP-0163: PEP (піктограми користувачів, OMEMO)</string>
     <string name="server_info_http_upload">XEP-0363: Обмін файлами через HTTP</string>
     <string name="server_info_push">XEP-0357: Push-повідомлення</string>
     <string name="server_info_available">так</string>
@@ -224,25 +235,29 @@
     <string name="delete_bookmark">Вилучити закладку</string>
     <string name="destroy_room">Вилучити груповий чат</string>
     <string name="destroy_channel">Закрити канал</string>
-    <string name="destroy_room_dialog">Дійсно вилучити цей груповий чат %s? Груповий чат буде вилучено з сервера.</string>
-    <string name="destroy_channel_dialog">Дійсно закрити цей публічний канал %s? Канал буде стерто, а дані вилучено з серверу.</string>
-    <string name="could_not_destroy_room">Вилучити груповий чат не вдалося</string>
+    <string name="destroy_room_dialog">Дійсно вилучити цей груповий чат\?
+\n
+\n<b>Увага:</b> Груповий чат буде вилучено, а дані стерто з сервера.</string>
+    <string name="destroy_channel_dialog">Дійсно закрити цей публічний канал\?
+\n
+\n<b>Увага:</b> Канал буде вилучено, а дані стерто з сервера.</string>
+    <string name="could_not_destroy_room">Не вдалося вилучити груповий чат</string>
     <string name="could_not_destroy_channel">Не вдалося закрити канал</string>
     <string name="action_edit_subject">Редагувати тему групи</string>
     <string name="topic">Тема</string>
     <string name="joining_conference">Приєднання до групи…</string>
     <string name="leave">Вийти</string>
-    <string name="contact_added_you">Контакт додано до вашого списку контактів</string>
+    <string name="contact_added_you">Контакт додав Вас до свого списку контактів</string>
     <string name="add_back">Також додати</string>
     <string name="contact_has_read_up_to_this_point">%s дочитав(ла) до цього місця</string>
-    <string name="contacts_have_read_up_to_this_point">%s прочитав(ла) до цього місця</string>
+    <string name="contacts_have_read_up_to_this_point">%s дочитали до цього місця</string>
     <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s +%2$d дочитали до цього місця</string>
     <string name="everyone_has_read_up_to_this_point">Усі прочитали до цього місця</string>
     <string name="publish">Опублікувати</string>
     <string name="touch_to_choose_picture">Торкніться піктограми користувача, щоб вибрати зображення з галереї</string>
     <string name="publishing">Публікація…</string>
-    <string name="error_publish_avatar_server_reject">Сервер відхилив вашу публікацію</string>
-    <string name="error_publish_avatar_converting">Щось пішло не так під час перетворення Вашого зображення</string>
+    <string name="error_publish_avatar_server_reject">Сервер відхилив Вашу публікацію</string>
+    <string name="error_publish_avatar_converting">Не вдалося конвертувати Ваше зображення</string>
     <string name="error_saving_avatar">Неможливо зберегти піктограму користувача на пристрій</string>
     <string name="or_long_press_for_default">(Або натисніть і тримайте, щоб скинути до значення за замовчуванням)</string>
     <string name="error_publish_avatar_no_server_support">Ваш сервер не підтримує публікацію піктограм користувачів</string>
@@ -258,10 +273,14 @@
     <string name="enable">Увімкнути</string>
     <string name="conference_requires_password">Група вимагає пароль</string>
     <string name="enter_password">Зазначте пароль</string>
-    <string name="request_presence_updates">Будь ласка, спершу надішліть запит на оновлення пристуності від Вашого контакта.\n\n<small>Оновлення буде використано, щоб визначити, яку програму-клієнт (які програми-клієнти) він використовує.</small></string>
+    <string name="request_presence_updates">Будь ласка, спершу надішліть запит на оновлення присутності від Вашого контакту.
+\n
+\n<small>Оновлення буде використано, щоб визначити, яку програму-клієнт (які програми-клієнти) він використовує</small>.</string>
     <string name="request_now">Надіслати запит зараз</string>
     <string name="ignore">Ігнорувати</string>
-    <string name="without_mutual_presence_updates"><b>Попередження:</b> Надсилання без взаємної згоди на оновлення пристуності може спричинити неочікувані проблеми.\n\n<small>Перегляньте деталі контакту та перевірте отримання стану присутности.</small></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_summary">Дозволити контактам редагувати свої повідомлення після надсилання</string>
@@ -281,10 +300,10 @@
     <string name="conference_kicked">Вас виключили з цієї групи</string>
     <string name="conference_shutdown">Цю групу закрили</string>
     <string name="conference_unknown_error">Ви більше не берете участі у цій групі</string>
-    <string name="using_account">використовується обліковий запис %s</string>
+    <string name="using_account">обліковий запис %s</string>
     <string name="hosted_on">розміщений на %s</string>
     <string name="checking_x">Перевіряю %s на вузлі з HTTP</string>
-    <string name="not_connected_try_again">Ви поза мережею. Спробуйте ще пізніше.</string>
+    <string name="not_connected_try_again">Ви поза мережею. Спробуйте ще раз пізніше</string>
     <string name="check_x_filesize">Перевірити %s розмір</string>
     <string name="check_x_filesize_on_host">Перевірити %1$s розмір на %2$s</string>
     <string name="message_options">Налаштування повідомлення</string>
@@ -292,7 +311,7 @@
     <string name="paste_as_quote">Вставити як цитату</string>
     <string name="copy_original_url">Копіювати оригінальний URL</string>
     <string name="send_again">Надіслати знову</string>
-    <string name="file_url">URL файла</string>
+    <string name="file_url">URL файлу</string>
     <string name="url_copied_to_clipboard">URL скопійовано</string>
     <string name="jabber_id_copied_to_clipboard">Адресу XMPP скопійовано</string>
     <string name="error_message_copied_to_clipboard">Текст повідомлення про помилку скопійовано</string>
@@ -312,7 +331,7 @@
     <string name="notification_backup_created_subtitle">Резервні копії збережено до %s</string>
     <string name="restoring_backup">Відтворення з резервної копії</string>
     <string name="notification_restored_backup_title">Відтворено з резервної копії</string>
-    <string name="notification_restored_backup_subtitle">Не забудьте увімкнути обліковий запис</string>
+    <string name="notification_restored_backup_subtitle">Не забудьте увімкнути обліковий запис.</string>
     <string name="choose_file">Файл</string>
     <string name="receiving_x_file">Отримання %1$s (%2$d%% завершено)</string>
     <string name="download_x_file">Завантажити %s</string>
@@ -320,33 +339,35 @@
     <string name="file">файл</string>
     <string name="open_x_file">Відкрити %s</string>
     <string name="sending_file">надсилання (%1$d%% завершено)</string>
-    <string name="preparing_file">Підготовка файлу до передачі</string>
+    <string name="preparing_file">Підготовка до передачі файлу</string>
     <string name="x_file_offered_for_download">%s запропоновано для завантаження</string>
     <string name="cancel_transmission">Припинити передачу</string>
-    <string name="file_transmission_failed">передача файла не вдалася</string>
+    <string name="file_transmission_failed">передача файлу не вдалася</string>
     <string name="file_transmission_cancelled">скасовано передачу файлу</string>
     <string name="file_deleted">Файл вилучено</string>
-    <string name="no_application_found_to_open_file">Не знайдено програми для відкриття файла</string>
-    <string name="no_application_found_to_open_link">Не знайдено застосунку для відкриття посилання</string>
-    <string name="no_application_found_to_view_contact">Не знайдено програми, щоб переглянути контакт</string>
+    <string name="no_application_found_to_open_file">Не знайдено застосунку, щоб відкрити файл</string>
+    <string name="no_application_found_to_open_link">Не знайдено застосунку, щоб відкрити посилання</string>
+    <string name="no_application_found_to_view_contact">Не знайдено застосунку, щоб переглянути контакт</string>
     <string name="pref_show_dynamic_tags">Динамічні позначки</string>
-    <string name="pref_show_dynamic_tags_summary">Показувати позначку \"лише для читання\" під контактами</string>
-    <string name="enable_notifications">Увікнути сповіщення</string>
+    <string name="pref_show_dynamic_tags_summary">Показувати позначку «лише для читання» під контактами</string>
+    <string name="enable_notifications">Увімкнути сповіщення</string>
     <string name="no_conference_server_found">Не знайдено сервер групи</string>
-    <string name="conference_creation_failed">Не вдалося створити групу!</string>
-    <string name="account_image_description">Іконка облікового запису</string>
+    <string name="conference_creation_failed">Не вдалося створити групу</string>
+    <string name="account_image_description">Піктограма облікового запису</string>
     <string name="copy_omemo_clipboard_description">Копіювати цифровий підпис OMEMO</string>
     <string name="regenerate_omemo_key">Повторно створити ключ OMEMO</string>
     <string name="clear_other_devices">Стерти пристрої</string>
-    <string name="clear_other_devices_desc">Ви певні, що хочете стерти всі інші пристрої з OMEMO-оголошення? Наступного разу, коли Ваші пристрої приєднаються, вони знову оголосять про себе, але вони можуть не отримати повідомлення, які можуть бути надіслані тим часом. </string>
-    <string name="error_no_keys_to_trust_server_error">Немає ключів, які б можна було використати для цього контакту.\nНе вдалося отримати нові ключі з сервера. Можливо, щось не так з сервером контактів.</string>
-    <string name="error_no_keys_to_trust_presence">Для цього контакту відсутні потрібні ключі.\nПеревірте, що ви обмінялися інформацією про доступність.</string>
+    <string name="clear_other_devices_desc">Ви впевнені, що хочете стерти всі інші пристрої з OMEMO-оголошення\? Наступного разу, коли Ваші пристрої приєднаються, вони знову оголосять про себе, але вони можуть не отримати повідомлення, надіслані тим часом.</string>
+    <string name="error_no_keys_to_trust_server_error">Немає ключів, які б можна було використати для цього контакту.
+\nНе вдалося отримати нові ключі з сервера. Можливо, щось не так із сервером контактів\?</string>
+    <string name="error_no_keys_to_trust_presence">Для цього контакту відсутні потрібні ключі.
+\nПеревірте, що ви обмінялися інформацією про присутність.</string>
     <string name="error_trustkeys_title">Щось пішло не так</string>
     <string name="fetching_history_from_server">Отримую історію з сервера</string>
     <string name="no_more_history_on_server">Більше немає історії на сервері</string>
     <string name="updating">Оновлюю…</string>
     <string name="password_changed">Пароль змінено!</string>
-    <string name="could_not_change_password">Не зміг змінити пароль</string>
+    <string name="could_not_change_password">Не вдалося змінити пароль</string>
     <string name="change_password">Змінити пароль</string>
     <string name="current_password">Поточний пароль</string>
     <string name="new_password">Новий пароль</string>
@@ -361,7 +382,7 @@
     <string name="advanced_mode">Розширений режим</string>
     <string name="grant_membership">Надати право участі</string>
     <string name="remove_membership">Відкликати право участі</string>
-    <string name="grant_admin_privileges">Дати права адміністратора</string>
+    <string name="grant_admin_privileges">Надати права адміністратора</string>
     <string name="remove_admin_privileges">Відкликати права адміністратора</string>
     <string name="grant_owner_privileges">Надати права власника</string>
     <string name="remove_owner_privileges">Відкликати права власника</string>
@@ -376,7 +397,7 @@
     <string name="conference_options">Налаштування приватного чату</string>
     <string name="channel_options">Налаштування публічного каналу</string>
     <string name="members_only">Приватно, лише для членів</string>
-    <string name="non_anonymous">Зробити XMPP адрес доступним для всіх</string>
+    <string name="non_anonymous">Зробити адреси XMPP доступними для всіх</string>
     <string name="moderated">Зробити канал модерованим</string>
     <string name="you_are_not_participating">Ви не берете участі</string>
     <string name="modified_conference_options">Налаштування групи змінено!</string>
@@ -388,31 +409,31 @@
     <string name="mark_as_read">Позначити як прочитане</string>
     <string name="pref_input_options">Введення</string>
     <string name="pref_enter_is_send">Enter для надсилання</string>
-    <string name="pref_enter_is_send_summary">Використовувати кнопку Enter для надсилання повідомлень. Якщо цей параметр вимкнено, повідомлення можна надсилати за допомогою комбінації Ctrl-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="audio">аудіо</string>
     <string name="video">відео</string>
     <string name="image">зображення</string>
     <string name="pdf_document">PDF документ</string>
-    <string name="apk">програма Android</string>
+    <string name="apk">Програма Android</string>
     <string name="vcard">Контакт</string>
-    <string name="avatar_has_been_published">Іконку користувача опубліковано!</string>
+    <string name="avatar_has_been_published">Піктограму користувача опубліковано!</string>
     <string name="sending_x_file">Надсилання %s</string>
     <string name="offering_x_file">Пропозиція %s</string>
     <string name="hide_offline">Сховати поза мережею</string>
     <string name="contact_is_typing">%s пише…</string>
     <string name="contact_has_stopped_typing">%s припинив писати</string>
     <string name="contacts_are_typing">%s пишуть…</string>
-    <string name="contacts_have_stopped_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="send_location">Розташування</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>
-    <string name="location">Розташування</string>
+    <string name="no_application_found_to_display_location">Не знайдено застосунку, щоб показати місцезнаходження</string>
+    <string name="location">Місцезнаходження</string>
     <string name="title_undo_swipe_out_conversation">Розмову закрито</string>
-    <string name="title_undo_swipe_out_group_chat">Полишити приватну групу обміну повідомленнями</string>
+    <string name="title_undo_swipe_out_group_chat">Залишити приватну групу</string>
     <string name="title_undo_swipe_out_channel">Залишити публічний канал</string>
     <string name="pref_dont_trust_system_cas_title">Не довіряти системним центрам сертифікації</string>
     <string name="pref_dont_trust_system_cas_summary">Усі сертифікати мають бути підтверджені вручну</string>
@@ -424,8 +445,8 @@
     <string name="dialog_manage_certs_negativebutton">Скасувати</string>
     <plurals name="toast_delete_certificates">
         <item quantity="one">%d сертифікат вилучено</item>
-        <item quantity="few">%d сертифікати видалено</item>
-        <item quantity="many">%d сертифікатів видалено</item>
+        <item quantity="few">%d сертифікати вилучено</item>
+        <item quantity="many">%d сертифікатів вилучено</item>
         <item quantity="other">%d сертифікатів вилучено</item>
     </plurals>
     <string name="pref_quick_action_summary">Замінювати кнопку надсилання швидкими діями</string>
@@ -435,7 +456,7 @@
     <string name="choose_quick_action">Вибрати швидку дію</string>
     <string name="search_contacts">Шукати в контактах</string>
     <string name="send_private_message">Приватне повідомлення</string>
-    <string name="user_has_left_conference">%1$s залишила групу!</string>
+    <string name="user_has_left_conference">%1$s залишив групу</string>
     <string name="username">Ім\'я користувача</string>
     <string name="username_hint">Ім\'я користувача</string>
     <string name="invalid_username">Таке ім\'я користувача не допустиме</string>
@@ -443,7 +464,7 @@
     <string name="download_failed_file_not_found">Звантаження не вдалося: файл не знайдено</string>
     <string name="download_failed_could_not_connect">Звантаження не вдалося: неможливо з\'єднатися з вузлом</string>
     <string name="download_failed_could_not_write_file">Завантаження не вдалося: неможливо записати файл</string>
-    <string name="account_status_tor_unavailable">Мережа ToR не доступна</string>
+    <string name="account_status_tor_unavailable">Мережа Tor недоступна</string>
     <string name="account_status_bind_failure">Прив\'язка не спрацювала</string>
     <string name="account_status_host_unknown">Сервер не відповідає за цей домен</string>
     <string name="server_info_broken">Поламано</string>
@@ -453,21 +474,21 @@
     <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="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="unable_to_fetch_mam_prefs">Не вдалося отримати налаштування збереження</string>
     <string name="captcha_required">Потрібно розв\'язати головоломку</string>
     <string name="captcha_hint">Уведіть текст із зображення вище</string>
     <string name="certificate_chain_is_not_trusted">Ланцюжок сертифікатів не довірений</string>
-    <string name="jid_does_not_match_certificate">XMPP адрес не відповідає сертифікату</string>
+    <string name="jid_does_not_match_certificate">Адреса XMPP не відповідає сертифікату</string>
     <string name="action_renew_certificate">Оновити сертифікат</string>
     <string name="error_fetching_omemo_key">Помилка отримання ключа OMEMO!</string>
     <string name="verified_omemo_key_with_certificate">Ключ OMEMO звірено з сертифікатом!</string>
     <string name="device_does_not_support_certificates">Ваш пристрій не підтримує вибір сертифікатів клієнта!</string>
     <string name="pref_connection_options">З\'єднання</string>
-    <string name="pref_use_tor">З\'єднання через ToR</string>
+    <string name="pref_use_tor">З\'єднання через Tor</string>
     <string name="pref_use_tor_summary">Тунелювати всі з\'єднання через мережу Tor. Потребує Orbot</string>
     <string name="account_settings_hostname">Назва вузла</string>
     <string name="account_settings_port">Порт</string>
@@ -482,36 +503,37 @@
         <item quantity="other">%d повідомлень</item>
     </plurals>
     <string name="load_more_messages">Завантажити більше повідомлень</string>
-    <string name="shared_file_with_x">Файлом поділилися з %s</string>
-    <string name="shared_image_with_x">Поділитися зображенням з %s</string>
-    <string name="shared_images_with_x">Поділитися зображеннями з %s</string>
-    <string name="shared_text_with_x">Текстом поділилися з %s</string>
+    <string name="shared_file_with_x">Файл надіслано %s</string>
+    <string name="shared_image_with_x">Зображення надіслано %s</string>
+    <string name="shared_images_with_x">Зображення надіслано %s</string>
+    <string name="shared_text_with_x">Текст надіслано %s</string>
     <string name="sync_with_contacts">Синхронізувати контакти</string>
     <string name="notify_on_all_messages">Сповіщати про всі повідомлення</string>
-    <string name="notify_only_when_highlighted">Повідомляти, лише якщо згадують</string>
+    <string name="notify_only_when_highlighted">Сповіщати, лише якщо згадують</string>
     <string name="notify_never">Сповіщення вимкнено</string>
     <string name="notify_paused">Сповіщення призупинено</string>
     <string name="pref_picture_compression">Стиснення зображень</string>
-    <string name="pref_picture_compression_summary">Підказка: Обирайте \"Вибрати файл\" замість \"Вибрати зображення\", щоб надіслати окремі зображення без стиснення в обхід цього налаштування.</string>
+    <string name="pref_picture_compression_summary">Підказка: Обирайте «Вибрати файл» замість «Вибрати зображення», щоб надіслати окремі зображення без стиснення в обхід цього налаштування.</string>
     <string name="always">Завжди</string>
     <string name="large_images_only">Лише великі зображення</string>
-    <string name="battery_optimizations_enabled">Оптимізацію батареї задіяно</string>
+    <string name="battery_optimizations_enabled">Увімкнено оптимізацію батареї</string>
     <string name="disable">Вимкнути</string>
     <string name="selection_too_large">Вибрана ділянка завелика</string>
-    <string name="no_accounts">(Немає активних облікових засобів)</string>
+    <string name="no_accounts">(Немає активних облікових записів)</string>
     <string name="this_field_is_required">Це обов\'язкове поле</string>
     <string name="correct_message">Відредагувати</string>
     <string name="send_corrected_message">Відредаговане повідомлення</string>
-    <string name="no_keys_just_confirm">Ви вже довіряєте цій особі. Вибираючи \"Готово\", ви лише підтверджуєте, що %s є учасником групи.</string>
+    <string name="no_keys_just_confirm">Ви вже довіряєте цій особі. Вибираючи «Зроблено», Ви лише підтверджуєте, що %s є учасником групи.</string>
     <string name="this_account_is_disabled">Ви вимкнули цей обліковий запис</string>
-    <string name="security_error_invalid_file_access">Помилка з безпекою: Неправильний доступ до файлу!</string>
-    <string name="no_application_to_share_uri">Не знайдено програми, щоб поділитися URI</string>
-    <string name="share_uri_with">Поділитися URI</string>
+    <string name="security_error_invalid_file_access">Помилка безпеки: неправильний доступ до файлу!</string>
+    <string name="no_application_to_share_uri">Не знайдено застосунку, щоб поділитися URI</string>
+    <string name="share_uri_with">Поділитися URI…</string>
     <string name="agree_and_continue">Погодитися та продовжити</string>
-    <string name="magic_create_text">Ми допоможемо вам створити обліковий запис. На наступній сторінці ви зможете змінити автоматично створений пароль.\nПодалі ви зможете спілкуватися з користувачами вашого або будь-якого іншого провайдера, для цього потрібно буде надати користувачеві вашу повну адресу XMPP.</string>
-    <string name="your_full_jid_will_be">Ваша повна адреса XMPP буде такою: %s</string>
+    <string name="magic_create_text">Ми допоможемо Вам створити обліковий запис на conversations.im.
+\nОбравши conversations.im в якості свого постачальника, Ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP.</string>
+    <string name="your_full_jid_will_be">Ваша повна адреса XMPP: %s</string>
     <string name="create_account">Створити обліковий запис</string>
-    <string name="use_own_provider">Застосувати дані мого власного провайдера</string>
+    <string name="use_own_provider">Застосувати дані мого власного постачальника</string>
     <string name="pick_your_username">Придумайте ім\'я користувача</string>
     <string name="pref_manually_change_presence">Керувати станом вручну</string>
     <string name="pref_manually_change_presence_summary">Показувати доступність під час редагування повідомлення зі статусом.</string>
@@ -524,7 +546,7 @@
     <string name="secure_password_generated">Створено надійний пароль</string>
     <string name="device_does_not_support_battery_op">Ваш пристрій не підтримує вимкнення оптимізації батареї</string>
     <string name="registration_please_wait">Реєстрація не вдалася: спробуйте ще раз пізніше</string>
-    <string name="registration_password_too_weak">Реєстрація не відбулася: пароль занадто слабкий.</string>
+    <string name="registration_password_too_weak">Реєстрація не відбулася: пароль занадто слабкий</string>
     <string name="choose_participants">Вибрати учасників</string>
     <string name="creating_conference">Створення групи…</string>
     <string name="invite_again">Запросити знову</string>
@@ -533,14 +555,14 @@
     <string name="gp_medium">Середній</string>
     <string name="gp_long">Довгий</string>
     <string name="pref_broadcast_last_activity">Показувати останню активність користувача</string>
-    <string name="pref_broadcast_last_activity_summary">Дозвольте всім вашим контактам знати, коли ви використовуєте месенджер</string>
+    <string name="pref_broadcast_last_activity_summary">Повідомляти співрозмовникам, що Ви користуєтеся Conversations</string>
     <string name="pref_privacy">Приватність</string>
     <string name="pref_theme_options">Тема</string>
     <string name="pref_theme_options_summary">Виберіть колір теми</string>
     <string name="pref_theme_automatic">Автоматично</string>
     <string name="pref_theme_light">Світла</string>
     <string name="pref_theme_dark">Темна</string>
-    <string name="unable_to_connect_to_keychain">Не можу зв\'язатися з OpenKeychain</string>
+    <string name="unable_to_connect_to_keychain">Не вдалося зв\'язатися з OpenKeychain</string>
     <string name="this_device_is_no_longer_in_use">Цей пристрій більше не використовується</string>
     <string name="type_pc">Комп\'ютер</string>
     <string name="type_phone">Мобільний телефон</string>
@@ -548,23 +570,23 @@
     <string name="type_web">Браузер</string>
     <string name="type_console">Консоль</string>
     <string name="payment_required">Вимагається оплата</string>
-    <string name="missing_internet_permission">Немає дозволу до користування Інтернет</string>
+    <string name="missing_internet_permission">Надати дозвіл на доступ до Інтернету</string>
     <string name="me">Я</string>
-    <string name="contact_asks_for_presence_subscription">Контакт просить надавати інформацію про вашу доступність</string>
+    <string name="contact_asks_for_presence_subscription">Контакт просить надавати інформацію про присутність</string>
     <string name="allow">Дозволити</string>
     <string name="no_permission_to_access_x">Немає дозволу на доступ до %s</string>
     <string name="remote_server_not_found">Віддалений сервер не знайдено</string>
     <string name="remote_server_timeout">Затримка відповіді сервера</string>
-    <string name="unable_to_update_account">Неможливо оновити обліковий запис</string>
+    <string name="unable_to_update_account">Не вдалося оновити обліковий запис</string>
     <string name="report_jid_as_spammer">Надіслати скаргу про те, що контакт з цим ID розсилає спам.</string>
     <string name="pref_delete_omemo_identities">Вилучити ідентифікаційні дані OMEMO</string>
-    <string name="pref_delete_omemo_identities_summary">Створити наново ваші ключі OMEMO. Усі ваші контакти будуть змушені підтвердити вас знову. Використовуйте це, лише якщо немає іншого вибору.</string>
+    <string name="pref_delete_omemo_identities_summary">Створити наново Ваші ключі OMEMO. Всі Ваші контакти будуть змушені підтвердити Вас знову. Використовуйте це, лише якщо немає іншого вибору.</string>
     <string name="delete_selected_keys">Вилучити вибрані ключі</string>
     <string name="error_publish_avatar_offline">Потрібно підключення, щоб можна було опублікувати піктограму користувача.</string>
     <string name="show_error_message">Показати повідомлення про помилку</string>
     <string name="error_message">Повідомлення про помилку</string>
-    <string name="data_saver_enabled">Увімкнено заощадження передачі даних</string>
-    <string name="error_unable_to_create_temporary_file">Неможливо створити тимчасовий файл</string>
+    <string name="data_saver_enabled">Увімкнено заощадження трафіку</string>
+    <string name="error_unable_to_create_temporary_file">Не вдалося створити тимчасовий файл</string>
     <string name="this_device_has_been_verified">Цей пристрій перевірено</string>
     <string name="copy_fingerprint">Копіювати цифровий підпис</string>
     <string name="barcode_does_not_contain_fingerprints_for_this_conversation">QR-код не містить цифрових підписів для цієї розмови.</string>
@@ -574,9 +596,9 @@
     <string name="share_as_barcode">Поділитися через QR-код </string>
     <string name="share_as_uri">Поділитися XMPP URI</string>
     <string name="share_as_http">Поділитися посиланням HTTP</string>
-    <string name="pref_blind_trust_before_verification">Довіряти новим пристроям</string>
-    <string name="pref_blind_trust_before_verification_summary">Автоматично довіряти усім новим пристроям співрозмовників, які ще не пройшли перевірки,  запитувати підтвердження вручну щоразу, як перевірений контакт додає свій новий пристрій</string>
-    <string name="blindly_trusted_omemo_keys">Ключі OMEMO наосліп прийнято як довірені</string>
+    <string name="pref_blind_trust_before_verification">Довіряти наосліп до підтвердження</string>
+    <string name="pref_blind_trust_before_verification_summary">Автоматично довіряти всім новим пристроям співрозмовників, які ще не пройшли перевірки, запитувати підтвердження вручну щоразу, як перевірений контакт додає свій новий пристрій.</string>
+    <string name="blindly_trusted_omemo_keys">Ключі OMEMO, яким Ви довіряєте наосліп, тобто співрозмовник може бути не тим, кому Ви довіряєте.</string>
     <string name="not_trusted">Недовірений</string>
     <string name="invalid_barcode">Недійсний QR-код </string>
     <string name="pref_clean_cache_summary">Очистити теку з кешем (використовується застосунком Камера)</string>
@@ -584,12 +606,13 @@
     <string name="pref_clean_private_storage">Очистити приватне сховище</string>
     <string name="pref_clean_private_storage_summary">Очистити приватне сховище, де зберігаються файли (Їх можна повторно звантажити з сервера)</string>
     <string name="i_followed_this_link_from_a_trusted_source">Я перейшов за цим посиланням від довіреного джерела</string>
-    <string name="verifying_omemo_keys_trusted_source">Ви збираєтеся підтвердити ключі OMEMO для %1$s після переходу за посиланням. Це безпечно, лише якщо ви отримали посилання з довіреного джерела, де лише %2$s міг опублікувати це посилання.</string>
-    <string name="verify_omemo_keys">Підтвердити цифрові підписи OMEMO</string>
+    <string name="verifying_omemo_keys_trusted_source">Ви збираєтеся підтвердити ключі OMEMO для %1$s після переходу за посиланням. Це безпечно, лише якщо Ви отримали посилання з довіреного джерела, де лише %2$s міг опублікувати це посилання.</string>
+    <string name="verify_omemo_keys">Підтвердити ключі OMEMO</string>
     <string name="show_inactive_devices">Показувати неактивні</string>
     <string name="hide_inactive_devices">Приховати неактивні</string>
     <string name="distrust_omemo_key">Не довіряти пристрою</string>
-    <string name="distrust_omemo_key_text">Ви певні, що більше не довіряєте цьому пристрою?\nЦей пристрій і повідомлення з нього будуть позначатися як недовірені.</string>
+    <string name="distrust_omemo_key_text">Ви впевнені, що більше не довіряєте цьому пристрою\?
+\nЦей пристрій і повідомлення з нього будуть позначатися як недовірені.</string>
     <plurals name="seconds">
         <item quantity="one">%d секунда</item>
         <item quantity="few">%d секунди</item>
@@ -634,7 +657,7 @@
     <string name="corresponding_conversations_closed">Відповідні розмови завершено.</string>
     <string name="contact_blocked_past_tense">Контакт заблоковано.</string>
     <string name="pref_notifications_from_strangers">Сповіщення від незнайомців</string>
-    <string name="pref_notifications_from_strangers_summary">Сповіщувати про повідомлення від незнайомців</string>
+    <string name="pref_notifications_from_strangers_summary">Сповіщати про повідомлення і виклики від незнайомців.</string>
     <string name="received_message_from_stranger">Отримано повідомлення від незнайомця</string>
     <string name="block_stranger">Заблокувати невідомий контакт</string>
     <string name="block_entire_domain">Заблокувати весь домен</string>
@@ -651,7 +674,7 @@
     <string name="yesterday">Учора</string>
     <string name="pref_validate_hostname">Перевіряти адресу за допомогою DNSSEC</string>
     <string name="pref_validate_hostname_summary">Сертифікати, які містять підтверджену адресу вузла, вважаються перевіреними</string>
-    <string name="certificate_does_not_contain_jid">Сертифікат не містить XMPP адресу</string>
+    <string name="certificate_does_not_contain_jid">Сертифікат не містить адреси XMPP</string>
     <string name="server_info_partial">частково</string>
     <string name="attach_record_video">Записати відео</string>
     <string name="copy_to_clipboard">Копіювати</string>
@@ -659,11 +682,11 @@
     <string name="message">Повідомлення</string>
     <string name="private_messages_are_disabled">Приватні повідомлення вимкнено</string>
     <string name="huawei_protected_apps">Захищені програми</string>
-    <string name="huawei_protected_apps_summary">Для отримання сповіщень, навіть коли екран погас, вам потрібно додати застосунок до списку застосунків, до яких не застосовується режим енергозбереження.</string>
+    <string name="huawei_protected_apps_summary">Щоб отримувати сповіщення навіть коли екран погас, необхідно додати Conversations до списку захищених програм.</string>
     <string name="mtm_accept_cert">Прийняти незнайомий сертифікат?</string>
-    <string name="mtm_trust_anchor">Сертифікат сервера не підтверджено відомим центром сертифікації </string>
+    <string name="mtm_trust_anchor">Сертифікат сервера не підтверджено відомим центром сертифікації.</string>
     <string name="mtm_accept_servername">Прийняти сервер з невідповідним ім\'ям?</string>
-    <string name="mtm_hostname_mismatch">Не вдається перевірити сервер як \"%s\". Сертифікат чинний лише для:</string>
+    <string name="mtm_hostname_mismatch">Серверу не вдалося авторизуватися як «%s». Сертифікат чинний лише для:</string>
     <string name="mtm_connect_anyway">Усе одно бажаєте підключитися?</string>
     <string name="mtm_cert_details">Деталі сертифіката:</string>
     <string name="once">Один раз</string>
@@ -675,41 +698,42 @@
     <string name="disable_encryption">Вимкнути шифрування</string>
     <string name="error_trustkey_device_list">Неможливо отримати перелік пристроїв</string>
     <string name="error_trustkey_bundle">Неможливо отримати пакети пристроїв</string>
-    <string name="error_trustkey_hint_mutual">Підказка: В деяких випадках це можна виправити, якщо додати один до одного у ваших списках контактів.</string>
-    <string name="disable_encryption_message">Дійсно вимкнути шифрування OMEMO?\nПісля цього у адміністратора вашого сервера буде можливість мати доступ до ваших повідомлень. Проте, це може бути єдиним способом спілкуватися з людьми, які використовують застарілі застосунки.</string>
+    <string name="error_trustkey_hint_mutual">Підказка: В деяких випадках це можна виправити, якщо додати один одного у свої списки контактів.</string>
+    <string name="disable_encryption_message">Дійсно вимкнути шифрування OMEMO\?
+\nПісля цього в адміністратора Вашого сервера буде можливість мати доступ до Ваших повідомлень. Проте, це може бути єдиним способом спілкуватися з людьми, які використовують застарілі застосунки.</string>
     <string name="disable_now">Вимкнути зараз</string>
     <string name="draft">Чернетка:</string>
     <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_on">Типово для нових розмов використовуватиметься OMEMO.</string>
+    <string name="pref_omemo_setting_summary_default_off">OMEMO потрібно буде вмикати окремо для кожної нової розмови.</string>
     <string name="create_shortcut">Створити ярлик</string>
     <string name="pref_font_size">Розмір шрифту</string>
-    <string name="pref_font_size_summary">Розмір шрифту у застосунку</string>
+    <string name="pref_font_size_summary">Розмір шрифту в застосунку.</string>
     <string name="default_on">Типово ввімкнено</string>
     <string name="default_off">Типово вимкнено</string>
     <string name="small">Малий</string>
     <string name="medium">Середній</string>
     <string name="large">Великий</string>
-    <string name="not_encrypted_for_this_device">Повідомлення не було зашифровано для цього пристрою</string>
+    <string name="not_encrypted_for_this_device">Повідомлення не було зашифровано для цього пристрою.</string>
     <string name="omemo_decryption_failed">Не вдалося розшифрувати повідомлення OMEMO.</string>
     <string name="undo">скасувати</string>
     <string name="location_disabled">Доступ до місцезнаходження вимкнено</string>
     <string name="action_fix_to_location">Закріпити розташування</string>
     <string name="action_unfix_from_location">Відкріпити розташування</string>
-    <string name="action_copy_location">Скопіювати розташування</string>
-    <string name="action_share_location">Поділитися розташуванням</string>
+    <string name="action_copy_location">Скопіювати місцезнаходження</string>
+    <string name="action_share_location">Поділитися місцезнаходженням</string>
     <string name="action_directions">Шлях</string>
-    <string name="title_activity_share_location">Поділитися розташуванням</string>
-    <string name="title_activity_show_location">Показати розташування</string>
+    <string name="title_activity_share_location">Поділитися місцезнаходженням</string>
+    <string name="title_activity_show_location">Показати місцезнаходження</string>
     <string name="share">Поділитися</string>
-    <string name="unable_to_start_recording">Не можу почати запис</string>
-    <string name="please_wait">Прошу зачекайте…</string>
+    <string name="unable_to_start_recording">Не вдалося почати запис</string>
+    <string name="please_wait">Будь ласка, зачекайте…</string>
     <string name="search_messages">Шукати в повідомленнях</string>
     <string name="gif">GIF</string>
     <string name="view_conversation">Переглянути розмову</string>
     <string name="pref_use_share_location_plugin">Додаток поширення місцезнаходження</string>
-    <string name="pref_use_share_location_plugin_summary">Використовувати додаток поширення місце-знаходження замість вбудованої карти</string>
+    <string name="pref_use_share_location_plugin_summary">Використовувати додаток поширення місцезнаходження замість вбудованої карти</string>
     <string name="copy_link">Копіювати посилання</string>
     <string name="copy_jabber_id">Копіювати адресу XMPP</string>
     <string name="p1_s3_filetransfer">Доступ до файлів через HTTP для S3</string>
@@ -735,8 +759,8 @@
     <string name="incoming_calls_channel_name">Вхідні виклики</string>
     <string name="ongoing_calls_channel_name">Активні виклики</string>
     <string name="silent_messages_channel_name">Тихі повідомлення</string>
-    <string name="silent_messages_channel_description">Ця група сповіщень показує сповіщення, які не повинні супроводжуватися звуком. Наприклад, у разі активности на іншому пристрої (період очікування).</string>
-    <string name="pref_message_notification_settings">Налаштування сповіщень повідомлень</string>
+    <string name="silent_messages_channel_description">Ця група сповіщень показує сповіщення, які не повинні супроводжуватися звуком. Наприклад, у разі активності на іншому пристрої (період очікування).</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="video_compression_channel_name">Стиснення відео</string>
@@ -754,67 +778,67 @@
     <string name="invalid_country_code">Недійсний код країни</string>
     <string name="choose_a_country">Виберіть країну</string>
     <string name="phone_number">номер телефону</string>
-    <string name="verify_your_phone_number">перевірити номер телефону</string>
-    <string name="enter_country_code_and_phone_number">Quicksy надішле SMS, щоб перевірити ваш номер телефону. Тарифами Вашого оператора може бути передбачено плату за отримання SMS. Зазначте код вашої країни та номер телефону:</string>
-    <string name="we_will_be_verifying"><![CDATA[Ми перевіримо номер телефону<br/><br/><b>%s</b><br/><br/>Чи все гаразд або ви бажаєте зазначити інший номер?]]></string>
+    <string name="verify_your_phone_number">Перевірте номер телефону</string>
+    <string name="enter_country_code_and_phone_number">Quicksy надішле SMS, щоб перевірити Ваш номер телефону. Тарифами Вашого оператора може бути передбачено плату за отримання SMS. Зазначте код Вашої країни та номер телефону:</string>
+    <string name="we_will_be_verifying">Ми перевіримо номер телефону<br/><br/><b>%s</b><br/><br/>Гаразд чи бажаєте вказати інший номер\?</string>
     <string name="not_a_valid_phone_number">%s не є дійсним номером телефону.</string>
-    <string name="please_enter_your_phone_number">Будь ласка, введіть ваш номер телефону.</string>
+    <string name="please_enter_your_phone_number">Будь ласка, введіть свій номер телефону.</string>
     <string name="search_countries">Шукати країну</string>
-    <string name="verify_x">Підтвердити %s</string>
-    <string name="we_have_sent_you_an_sms_to_x"><![CDATA[Ми направили вам SMS на <b>%s</b>.]]></string>
-    <string name="we_have_sent_you_another_sms">Ми направили вам SMS із кодом з 6 цифр</string>
+    <string name="verify_x">Перевірити %s</string>
+    <string name="we_have_sent_you_an_sms_to_x">Ми направили Вам SMS на <b>%s</b>.</string>
+    <string name="we_have_sent_you_another_sms">Ми направили Вам SMS із кодом з 6 цифр.</string>
     <string name="please_enter_pin_below">Будь ласка, введіть нижче 6 цифр коду.</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">Код, автоматично вставлений з буферу обміну.</string>
-    <string name="please_enter_pin">Будь ласка, введіть нижче ваш код з 6 цифр.</string>
-    <string name="abort_registration_procedure">Ви певні, що хочете припинити процедуру реєстрації?</string>
+    <string name="possible_pin">Код автоматично вставлений з буфера обміну.</string>
+    <string name="please_enter_pin">Будь ласка, введіть нижче код із 6 цифр.</string>
+    <string name="abort_registration_procedure">Ви впевнені, що хочете припинити процедуру реєстрації\?</string>
     <string name="yes">Так</string>
     <string name="no">Ні</string>
-    <string name="verifying">Підтверджую…</string>
+    <string name="verifying">Перевірка…</string>
     <string name="requesting_sms">Запит SMS…</string>
-    <string name="incorrect_pin">Код, який ви ввели, не правильний.</string>
-    <string name="pin_expired">Код, який ви ввели, застарів.</string>
+    <string name="incorrect_pin">Код, який Ви ввели, неправильний.</string>
+    <string name="pin_expired">Код, який Ви ввели, застарів.</string>
     <string name="unknown_api_error_network">Невідома помилка мережі.</string>
     <string name="unknown_api_error_response">Сервер дав незрозумілу відповідь.</string>
     <string name="unable_to_connect_to_server">Неможливо приєднатися до сервера.</string>
     <string name="unable_to_establish_secure_connection">Неможливо встановити безпечне з\'єднання.</string>
     <string name="unable_to_find_server">Неможливо знайти сервер.</string>
-    <string name="something_went_wrong_processing_your_request">Щось пішло не так під час обробки вашого запиту.</string>
+    <string name="something_went_wrong_processing_your_request">Щось пішло не так під час обробки Вашого запиту.</string>
     <string name="invalid_user_input">Користувач зазначив неправильні дані</string>
     <string name="temporarily_unavailable">Тимчасово недоступний. Будь ласка, спробуйте знову пізніше.</string>
     <string name="no_network_connection">Відсутнє з\'єднання з мережею.</string>
     <string name="try_again_in_x">Будь ласка, спробуйте ще раз через %s</string>
     <string name="rate_limited">Ви обмежені за рейтингом</string>
     <string name="too_many_attempts">Забагато спроб</string>
-    <string name="the_app_is_out_of_date">Версія вашого застосунку застаріла</string>
+    <string name="the_app_is_out_of_date">Версія Вашого застосунку застаріла.</string>
     <string name="update">Оновити</string>
     <string name="logged_in_with_another_device">Зараз цей номер телефону авторизований на іншому пристрої.</string>
-    <string name="enter_your_name_instructions">Будь ласка, зазначте ваше ім\'я, щоб надати можливість людям, які не мають вашого контакту, дізналися, хто ви є.</string>
+    <string name="enter_your_name_instructions">Будь ласка, вкажіть своє ім\'я, щоб надати можливість людям, які не мають Вашого контакту, дізнатися, хто Ви є.</string>
     <string name="your_name">Ваше ім\'я</string>
-    <string name="enter_your_name">Зазначте ваше ім\'я</string>
+    <string name="enter_your_name">Вкажіть своє ім\'я</string>
     <string name="no_name_set_instructions">Відредагуйте ім\'я користувача.</string>
     <string name="reject_request">Відхилити запит</string>
     <string name="install_orbot">Встановити Orbot</string>
     <string name="start_orbot">Запустити Orbot</string>
     <string name="no_market_app_installed">Не знайдено застосунку для пошуку й встановлення нових застосунків.</string>
-    <string name="group_chat_will_make_your_jabber_id_public">Цей канал опублікує вашу адресу XMPP</string>
+    <string name="group_chat_will_make_your_jabber_id_public">Цей канал опублікує Вашу адресу XMPP</string>
     <string name="ebook">Електронна книга</string>
-    <string name="video_original">Оригінал (не стиснений)</string>
-    <string name="open_with">Відкрити</string>
-    <string name="set_profile_picture">Світлана профілю</string>
+    <string name="video_original">Оригінал (нестиснений)</string>
+    <string name="open_with">Відкрити…</string>
+    <string name="set_profile_picture">Зображення профілю для Conversations</string>
     <string name="choose_account">Виберіть обліковий запис</string>
     <string name="restore_backup">Відновити з резервної копії</string>
     <string name="restore">Відновити</string>
     <string name="enter_password_to_restore">Зазначте пароль до облікового запису %s, щоб відновити з резервної копії.</string>
     <string name="restore_warning">Не використовуйте відновлення з резервної копії з метою клонування застосунку (запускати одночасно ще один примірник). Відновлення з резервної копії призначене виключно для перенесення даних або на випадок втрати оригінального пристрою.</string>
     <string name="unable_to_restore_backup">Неможливо відновити з резервної копії.</string>
-    <string name="unable_to_decrypt_backup">Неможливл розшифрувати резервну копію. Чи пароль правильний?</string>
+    <string name="unable_to_decrypt_backup">Неможливо розшифрувати резервну копію. Чи правильний пароль\?</string>
     <string name="backup_channel_name">Створити або відновити резервну копію</string>
-    <string name="enter_jabber_id">Ввести адресу XMPP</string>
-    <string name="create_group_chat">Створити групу обміну повідомленнями</string>
+    <string name="enter_jabber_id">Введіть адресу XMPP</string>
+    <string name="create_group_chat">Створити групу</string>
     <string name="join_public_channel">Приєднатися до публічного каналу</string>
     <string name="create_private_group_chat">Створити приватну групу</string>
     <string name="create_public_channel">Створити публічний канал</string>
@@ -826,7 +850,7 @@
     <string name="creating_channel">Створення публічного каналу…</string>
     <string name="channel_already_exists">Цей канал уже існує</string>
     <string name="joined_an_existing_channel">Ви приєдналися до наявного каналу</string>
-    <string name="unable_to_set_channel_configuration">Неможливо налаштувати канал</string>
+    <string name="unable_to_set_channel_configuration">Не вдалося зберегти налаштування каналу</string>
     <string name="allow_participants_to_edit_subject">Дозволити будь-кому редагувати тему</string>
     <string name="allow_participants_to_invite_others">Дозволити будь-кому запрошувати інших</string>
     <string name="anyone_can_edit_subject">Будь-хто може редагувати тему.</string>
@@ -836,8 +860,8 @@
     <string name="anyone_can_invite_others">Будь-хто може запрошувати інших.</string>
     <string name="jabber_ids_are_visible_to_admins">Адміністратори бачать адреси XMPP.</string>
     <string name="jabber_ids_are_visible_to_anyone">Будь-хто бачить адреси XMPP.</string>
-    <string name="no_users_hint_channel">Цей публічний канал не має учасників. Запросіть ваші контакти або скористайтеся кнопкою поширення, щоб поділитися адресою XMPP.</string>
-    <string name="no_users_hint_group_chat">У цій приватній групі обміну повідомленнями відсутні учасники.</string>
+    <string name="no_users_hint_channel">Цей публічний канал не має учасників. Запросіть Ваші контакти або скористайтеся кнопкою поширення, щоб поділитися адресою XMPP.</string>
+    <string name="no_users_hint_group_chat">У цій приватній групі відсутні учасники.</string>
     <string name="manage_permission">Керувати правами</string>
     <string name="search_participants">Шукати учасників</string>
     <string name="file_too_large">Файл надто великий</string>
@@ -864,7 +888,7 @@
     <string name="group_chats_and_channels"><![CDATA[Групи і канали]]></string>
     <string name="jabber_network">jabber.network</string>
     <string name="local_server">Локальний сервер</string>
-    <string name="pref_channel_discovery_summary">Більшість користувачів вибирають \'jabber.network\' як одну з кращих пропозицій зі всіх публічних середовищ XMPP.</string>
+    <string name="pref_channel_discovery_summary">Більшість користувачів вибирають jabber.network як одну з кращих пропозицій з усіх публічних середовищ XMPP.</string>
     <string name="pref_channel_discovery">Спосіб пошуку каналів</string>
     <string name="backup">Резервне копіювання</string>
     <string name="category_about">Про застосунок</string>
@@ -888,10 +912,10 @@
     <string name="hang_up">Завершити</string>
     <string name="ongoing_call">Активний виклик</string>
     <string name="ongoing_video_call">Активний відеовиклик</string>
-    <string name="disable_tor_to_make_call">Вимкнути ToR для здійснення викликів</string>
+    <string name="disable_tor_to_make_call">Вимкнути Tor для здійснення викликів</string>
     <string name="incoming_call">Вхідний виклик</string>
     <string name="outgoing_call">Вихідний виклик</string>
-    <string name="missed_call">Пропущені виклики</string>
+    <string name="missed_call">Пропущений виклик</string>
     <string name="audio_call">Голосовий виклик</string>
     <string name="video_call">Відеовиклик</string>
     <string name="help">Допомога</string>
@@ -906,4 +930,139 @@
         <item quantity="many">Перегляд %1$d учасників</item>
         <item quantity="other">Перегляд %1$d учасників</item>
     </plurals>
-    </resources>
+    <plurals name="n_missed_calls_from_m_contacts">
+        <item quantity="one">%1$d пропущений виклик від %2$d контакту</item>
+        <item quantity="few">%1$d пропущених виклики від %2$d контактів</item>
+        <item quantity="many">%1$d пропущених викликів від %2$d контактів</item>
+        <item quantity="other">%1$d пропущених викликів від %2$d контактів</item>
+    </plurals>
+    <string name="action_end_conversation">Закрити розмову</string>
+    <string name="crash_report_title">Збій %1$s</string>
+    <string name="crash_report_message">Надсилаючи зі свого облікового запису XMPP траси стеку викликів, Ви допомагаєте розробляти %1$s.</string>
+    <string name="pref_call_ringtone_summary">Мелодія для вхідних викликів</string>
+    <string name="pref_prevent_screenshots">Заборонити знімки екрана</string>
+    <string name="pref_prevent_screenshots_summary">Ховати вміст застосунку при перемиканні програм і заборонити знімки екрана</string>
+    <string name="account_status_incompatible_client">Несумісний клієнт</string>
+    <string name="account_status_tls_error_domain">Домен неможливо перевірити</string>
+    <string name="pref_up_push_account_summary">Обліковий запис для отримання push-повідомлень.</string>
+    <string name="pref_up_push_server_title">Сервер push</string>
+    <plurals name="n_missed_calls_from_x">
+        <item quantity="one">%1$d пропущений виклик від %2$s</item>
+        <item quantity="few">%1$d пропущених виклики від %2$s</item>
+        <item quantity="many">%1$d пропущених викликів від %2$s</item>
+        <item quantity="other">%1$d пропущених викликів від %2$s</item>
+    </plurals>
+    <string name="download_failed_invalid_file">Звантаження не вдалося: неправильний файл</string>
+    <string name="pref_dnd_on_silent_mode_summary">Встановити статус «Зайнятий», коли пристрій у безшумному режимі</string>
+    <string name="rtp_state_content_add_video">Перемкнути на відеовиклик\?</string>
+    <string name="no_storage_permission">Надати %1$s доступ до зовнішньої пам\'яті</string>
+    <plurals name="n_missed_calls">
+        <item quantity="one">%d пропущений виклик</item>
+        <item quantity="few">%d пропущених виклики</item>
+        <item quantity="many">%d пропущених викликів</item>
+        <item quantity="other">%d пропущених викликів</item>
+    </plurals>
+    <string name="gpx_track">Трек GPX</string>
+    <string name="record_voice_mail">Записати голосове повідомлення</string>
+    <plurals name="x_unread_conversations">
+        <item quantity="one">%d непрочитана розмова</item>
+        <item quantity="few">%d непрочитаних розмови</item>
+        <item quantity="many">%d непрочитаних розмов</item>
+        <item quantity="other">%d непрочитаних розмов</item>
+    </plurals>
+    <string name="search_this_conversation">Ця розмова</string>
+    <string name="add_contact_or_create_or_join_group_chat">Додати контакт, створити чи приєднатися до групи або знайти канали</string>
+    <string name="pref_up_push_account_title">Обліковий запис XMPP</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>
+    <string name="outgoing_call_timestamp">Вихідний виклик · %s</string>
+    <string name="openkeychain_required_long">%1$s використовує &lt;b&gt;OpenKeychain&lt;/b&gt; для шифрування/дешифрування повідомлень і керування публічними ключами.&lt;br&gt;&lt;br&gt;OpenKeychain поширюється на умовах ліцензії GPLv3+ і доступний для завантаження на F-Droid та Google Play.&lt;br&gt;&lt;br&gt;&lt;small&gt;(Після встановлення необхідно перезапустити %1$s.)&lt;/small&gt;</string>
+    <string name="more_options">Додатково</string>
+    <string name="unable_to_enable_video">Не вдалося увімкнути відео.</string>
+    <string name="no_active_accounts_support_this">Жоден з активних облікових записів не підтримує цієї функції</string>
+    <string name="delete_from_server">Вилучити обліковий запис із сервера</string>
+    <plurals name="some_messages_could_not_be_delivered">
+        <item quantity="one">Не вдалося надіслати повідомлення</item>
+        <item quantity="few">Деякі повідомлення не вдалося надіслати</item>
+        <item quantity="many">Деякі повідомлення не вдалося надіслати</item>
+        <item quantity="other">Деякі повідомлення не вдалося надіслати</item>
+    </plurals>
+    <string name="pref_up_push_server_summary">Оберіть сервер для доставки push-повідомлень через XMPP на Ваш пристрій.</string>
+    <string name="error_security_exception">Застосунок для передачі зображення не надав достатніх дозволів.</string>
+    <string name="audio_video_disabled_tor">Виклики вимкнені при використанні Tor</string>
+    <string name="sync_with_contacts_long">%1$s потребує дозволу на доступ до контактів, щоб порівняти їх з Вашими XMPP-контактами.
+\nТаким чином можна буде показувати піктограми і повні імена користувачів.
+\n
+\n%1$s співставлення інформації про контакти відбувається локально, нічого не завантажується на сервер.</string>
+    <string name="missed_calls_channel_name">Пропущені виклики</string>
+    <string name="data_saver_enabled_explained">Ваша операційна система обмежує для %1$s доступ до Інтернету у фоновому режимі. Щоб отримувати сповіщення про нові повідомлення, Вам потрібно дозволити %1$s необмежений доступ, коли заощадження трафіку увімкнено.
+\n%1$s намагатиметься по можливості економити трафік.</string>
+    <string name="pref_autojoin_summary">Встановлювати прапорець «autojoin» під час приєднання або виходу з групового чату/каналу та реагувати на зміни, зроблені іншими клієнтами.</string>
+    <string name="battery_optimizations_enabled_explained">Ваш пристрій застосовує до %1$s інтенсивний режим енергозбереження, що може спричинити затримку сповіщень чи навіть втрату повідомлень.
+\nРекомендуємо вимкнути режим енергозбереження.</string>
+    <string name="could_not_correct_message">Не вдалося відредагувати повідомлення</string>
+    <string name="omemo_fingerprint_selected_message">Цифровий підпис OMEMO (джерело повідомлення)</string>
+    <string name="omemo_fingerprint_x509_selected_message">Цифровий підпис v\\OMEMO (джерело повідомлення)</string>
+    <string name="continue_btn">Продовжити</string>
+    <string name="group_chats">Групові чати</string>
+    <string name="save_as_group_chat">Зберегти як групу</string>
+    <string name="pref_autojoin">Синхронізувати закладки</string>
+    <string name="conference_technical_problems">Ви залишили цю групу з технічних причин</string>
+    <string name="vector_graphic">векторна графіка</string>
+    <string name="multimedia_file">мультимедіа</string>
+    <string name="search_group_chats">Шукати групу</string>
+    <string name="pref_away_when_screen_off">«Відійшов» якщо екран заблоковано</string>
+    <string name="pref_away_when_screen_off_summary">Встановити статус «Відійшов», коли пристрій заблоковано</string>
+    <string name="pref_dnd_on_silent_mode">«Зайнятий» у безшумному режимі</string>
+    <string name="pref_treat_vibrate_as_dnd_summary">Встановити статус «Зайнятий», коли пристрій у віброрежимі</string>
+    <string name="no_camera_permission">Надати %1$s доступ до камери</string>
+    <string name="battery_optimizations_enabled_dialog">Ваш пристрій застосовує до %1$s інтенсивний режим енергозбереження, що може спричинити затримку сповіщень чи навіть втрату повідомлень.
+\n
+\nЗараз з\'явиться запит на вимкнення режиму енергозбереження.</string>
+    <string name="verifying_omemo_keys_trusted_source_account">Ви збираєтеся підтвердити ключі OMEMO для власного облікового запису. Це безпечно, лише якщо Ви перейшли за посиланням з довіреного джерела, де лише Ви могли опублікувати це посилання.</string>
+    <string name="all_omemo_keys_have_been_verified">Ви підтвердили всі ключі OMEMO, якими володієте</string>
+    <string name="device_does_not_support_data_saver">Ваш пристрій не підтримує вимкнення заощадження трафіку для %1$s.</string>
+    <string name="error_trustkey_general">%1$s не вдалося надіслати зашифроване повідомлення для %2$s. Імовірно, контакт використовує застарілий сервер або програму, яка не підтримує OMEMO.</string>
+    <string name="no_microphone_permission">Надати %1$s доступ до мікрофона</string>
+    <string name="delivery_failed_channel_name">Ненадіслані повідомлення</string>
+    <string name="foreground_service_channel_description">Показує постійне сповіщення про те, що %1$s працює.</string>
+    <string name="missed_call_timestamp">Пропущений виклик · %s</string>
+    <string name="incoming_call_duration_timestamp">Вхідний виклик (%s) · %s</string>
+    <string name="outgoing_call_duration_timestamp">Вихідний виклик (%s) · %s</string>
+    <string name="rtp_state_reconnecting">Повторне з\'єднання</string>
+    <string name="reconnecting_call">Повторний виклик</string>
+    <string name="reconnecting_video_call">Повторний відеовиклик</string>
+    <string name="rtp_state_security_error">Проблема при перевірці</string>
+    <string name="add_to_favorites">Закріпити</string>
+    <string name="remove_from_favorites">Відкріпити</string>
+    <string name="search_all_conversations">Усі розмови</string>
+    <string name="encrypted_with_omemo">Зашифровано за допомогою OMEMO</string>
+    <string name="encrypted_with_openpgp">Зашифровано за допомогою OpenPGP</string>
+    <string name="not_encrypted">Не зашифровано</string>
+    <string name="exit">Вийти</string>
+    <string name="invite_to_app">Запросити до Conversations</string>
+    <string name="plain_text_document">Текстовий документ</string>
+    <string name="no_xmpp_adddress_found">Не знайдено адреси XMPP</string>
+    <string name="no_account_deactivated">Немає (вимкнено)</string>
+    <string name="decline">Відхилити</string>
+    <string name="your_avatar">Ваша піктограма</string>
+    <string name="avatar_for_x">Піктограма для %s</string>
+    <string name="delete_avatar">Вилучити піктограму</string>
+    <string name="unable_to_parse_invite">Не вдалося обробити запрошення</string>
+    <string name="server_does_not_support_easy_onboarding_invites">Створення запрошень не підтримується сервером</string>
+    <string name="account_registrations_are_not_supported">Реєстрація облікових записів не підтримується</string>
+    <string name="switch_to_video">Перемкнути на відео</string>
+    <string name="reject_switch_to_video">Відхиляти запит перемкнути на відео</string>
+    <string name="could_not_delete_account_from_server">Не вдалося вилучити обліковий запис із сервера</string>
+    <string name="failed_deliveries">Ненадіслані повідомлення</string>
+    <string name="backup_started_message">Створення резервної копії. Ви отримаєте сповіщення щойно резервування завершиться.</string>
+    <string name="account_status_temporary_auth_failure">Тимчасова помилка автентифікації</string>
+    <string name="play_audio">Відтворити аудіо</string>
+    <string name="pause_audio">Зупинити аудіо</string>
+    <string name="no_application_found">Не знайдено застосунку</string>
+    <string name="unified_push_distributor">Дистриб\'ютор UnifiedPush</string>
+    <string name="rtp_state_content_add">Додати ще пісні\?</string>
+    <string name="restore_warning_continued">Не намагайтеся відновити резервні копії, які створили не Ви!</string>
+    <string name="outdated_backup_file_format">Ви намагаєтеся імпортувати файл резервної копії у застарілому форматі</string>
+    <string name="audiobook">Аудіокнига</string>
+    <string name="reconnect_on_other_host">Відновити з\'єднання на іншому вузлі</string>
+</resources>

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

@@ -2,58 +2,58 @@
 <resources>
     <string name="action_settings">设置</string>
     <string name="action_add">新对话</string>
-    <string name="action_accounts">管理账户</string>
-    <string name="action_account">管理账户</string>
-    <string name="action_end_conversation">关闭 Conversation</string>
+    <string name="action_accounts">管理账号</string>
+    <string name="action_account">管理账号</string>
+    <string name="action_end_conversation">关闭对话</string>
     <string name="action_contact_details">联系人详情</string>
     <string name="action_muc_details">群聊详情</string>
     <string name="channel_details">频道详情</string>
-    <string name="action_add_account">添加账户</string>
+    <string name="action_add_account">添加账号</string>
     <string name="action_edit_contact">编辑名称</string>
     <string name="action_add_phone_book">添加到通讯录</string>
-    <string name="action_delete_contact">从通讯录中删除</string>
-    <string name="action_block_contact">封禁联系人</string>
-    <string name="action_unblock_contact">解封联系人</string>
-    <string name="action_block_domain">封禁域名</string>
-    <string name="action_unblock_domain">解封域名</string>
-    <string name="action_block_participant">封禁成员</string>
-    <string name="action_unblock_participant">解封成员</string>
-    <string name="title_activity_manage_accounts">管理账户</string>
+    <string name="action_delete_contact">从联系人列表删除</string>
+    <string name="action_block_contact">屏蔽联系人</string>
+    <string name="action_unblock_contact">解除屏蔽联系人</string>
+    <string name="action_block_domain">屏蔽域名</string>
+    <string name="action_unblock_domain">解除屏蔽域名</string>
+    <string name="action_block_participant">屏蔽参与者</string>
+    <string name="action_unblock_participant">解除屏蔽参与者</string>
+    <string name="title_activity_manage_accounts">管理账号</string>
     <string name="title_activity_settings">设置</string>
-    <string name="title_activity_sharewith">通过Conversations分享</string>
+    <string name="title_activity_sharewith">分享至对话</string>
     <string name="title_activity_start_conversation">开始对话</string>
     <string name="title_activity_choose_contact">选择联系人</string>
     <string name="title_activity_choose_contacts">选择联系人</string>
-    <string name="title_activity_share_via_account">通过账户分享</string>
-    <string name="title_activity_block_list">封禁列表</string>
-    <string name="just_now">刚刚</string>
-    <string name="minute_ago">1分钟前</string>
-    <string name="minutes_ago">%d分钟前</string>
+    <string name="title_activity_share_via_account">通过账号分享</string>
+    <string name="title_activity_block_list">屏蔽列表</string>
+    <string name="just_now">刚才</string>
+    <string name="minute_ago">1 分钟前</string>
+    <string name="minutes_ago">%d 分钟前</string>
     <plurals name="x_unread_conversations">
-        <item quantity="other">%d 未读会话</item>
+        <item quantity="other">%d 个未读对话</item>
     </plurals>
-    <string name="sending">发送中…</string>
-    <string name="message_decrypting">正在解密信息。请稍候……</string>
-    <string name="pgp_message">OpenPGP加密的信息</string>
-    <string name="nick_in_use">用户名已存在</string>
-    <string name="invalid_muc_nick">无效用户名</string>
+    <string name="sending">正在发送…</string>
+    <string name="message_decrypting">解密消息中。请稍候…</string>
+    <string name="pgp_message">OpenPGP 加密消息</string>
+    <string name="nick_in_use">此昵称已在使用中</string>
+    <string name="invalid_muc_nick">昵称无效</string>
     <string name="admin">管理员</string>
     <string name="owner">所有者</string>
-    <string name="moderator">群主</string>
-    <string name="participant">成员</string>
+    <string name="moderator">主持人</string>
+    <string name="participant">参与者</string>
     <string name="visitor">访客</string>
-    <string name="remove_contact_text">将%s从XMPP联系人中移除?与该联系人的会话消息不会清除。</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="contact_blocked">联系人已封禁</string>
-    <string name="blocked">已封禁</string>
-    <string name="remove_bookmark_text">从书签中移除%s?相关会话消息不会被清除。</string>
-    <string name="register_account">在服务器上注册新账户</string>
+    <string name="remove_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="contact_blocked">联系人已屏蔽</string>
+    <string name="blocked">已屏蔽</string>
+    <string name="remove_bookmark_text">是否从书签中移除 %s?将不会移除与此书签相关的对话。</string>
+    <string name="register_account">在服务器上注册新账号</string>
     <string name="change_password_on_server">在服务器上修改密码</string>
-    <string name="share_with">分享…</string>
-    <string name="start_conversation">开始聊天</string>
+    <string name="share_with">分享至…</string>
+    <string name="start_conversation">开始对话</string>
     <string name="invite_contact">邀请联系人</string>
     <string name="invite">邀请</string>
     <string name="contacts">联系人</string>
@@ -63,53 +63,59 @@
     <string name="add">添加</string>
     <string name="edit">编辑</string>
     <string name="delete">删除</string>
-    <string name="block">封禁</string>
-    <string name="unblock">解封</string>
+    <string name="block">屏蔽</string>
+    <string name="unblock">解除屏蔽</string>
     <string name="save">保存</string>
     <string name="ok">完成</string>
-    <string name="crash_report_title">%1$s已崩溃</string>
-    <string name="crash_report_message">用你的 XMPP 账户发送堆栈跟踪来帮助持续开发 %1$s。</string>
+    <string name="crash_report_title">%1$s 已崩溃</string>
+    <string name="crash_report_message">使用 XMPP 账号发送堆栈跟踪有助于 %1$s 的持续开发。</string>
     <string name="send_now">立即发送</string>
     <string name="send_never">不再询问</string>
-    <string name="problem_connecting_to_account">账户无法连接</string>
-    <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="problem_connecting_to_account">无法连接到账号</string>
+    <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="add_contact">添加联系人</string>
     <string name="send_failed">传递失败</string>
-    <string name="preparing_image">准备发送图片</string>
-    <string name="preparing_images">准备发送图片</string>
-    <string name="sharing_files_please_wait">正在分享文件,请稍候…</string>
-    <string name="action_clear_history">清除历史记录</string>
-    <string name="clear_conversation_history">清除聊天记录</string>
-    <string name="clear_histor_msg">您确定要删除此聊天中的所有消息吗?\n\n<b>警告:</b>这不会删除存储在其他设备或服务器上的那些消息的副本。</string>
+    <string name="preparing_image">正在准备发送图片</string>
+    <string name="preparing_images">正在准备发送图片</string>
+    <string name="sharing_files_please_wait">分享文件中。请稍候…</string>
+    <string name="action_clear_history">清空历史记录</string>
+    <string name="clear_conversation_history">清空对话历史记录</string>
+    <string name="clear_histor_msg">是否要删除此对话中的所有消息?
+\n
+\n<b>警告:</b>存储在其他设备或服务器上的消息将不受影响。</string>
     <string name="delete_file_dialog">删除文件</string>
-    <string name="delete_file_dialog_msg">您确定要删除此文件吗?
+    <string name="delete_file_dialog_msg">是否确定要删除此文件?
 \n
-\n<b>警告:</b>这不会删除存储在其他设备或服务器上的此文件的副本。 </string>
-    <string name="also_end_conversation">之后关闭此聊天</string>
+\n<b>警告:</b>存储在其他设备或服务器上此文件的副本将不会删除。 </string>
+    <string name="also_end_conversation">之后关闭此对话</string>
     <string name="choose_presence">选择设备</string>
-    <string name="send_unencrypted_message">发送未加密的信息</string>
-    <string name="send_message">发送信息</string>
-    <string name="send_message_to_x">发信息给%s</string>
-    <string name="send_omemo_message">发送OMEMO加密信息</string>
-    <string name="send_omemo_x509_message">发送v\\OMEMO加密信息</string>
-    <string name="send_pgp_message">发送OpenPGP加密信息</string>
-    <string name="your_nick_has_been_changed">昵称已被使用</string>
-    <string name="send_unencrypted">不加密发送</string>
-    <string name="decryption_failed">解密失败,可能是私钥不正确。</string>
+    <string name="send_unencrypted_message">发送未加密消息</string>
+    <string name="send_message">发送消息</string>
+    <string name="send_message_to_x">发送消息至 %s</string>
+    <string name="send_omemo_message">发送 OMEMO 加密消息</string>
+    <string name="send_omemo_x509_message">发送 v\\OMEMO 加密消息</string>
+    <string name="send_pgp_message">发送 OpenPGP 加密消息</string>
+    <string name="your_nick_has_been_changed">正在使用新昵称</string>
+    <string name="send_unencrypted">发送未加密的</string>
+    <string name="decryption_failed">解密失败。也许您没有正确的私钥。</string>
     <string name="openkeychain_required">OpenKeychain</string>
-    <string name="openkeychain_required_long"><![CDATA[%1$s使用 <b>OpenKeychain</b>来加密和解密消息并管理你的公钥。<br><br>它在 GPLv3+ 许可证下授权并可在 F-Droid 和 Google Play 上获得。<br><br><small>(请之后重启 %1$s)</small>]]></string>
-    <string name="restart">重启</string>
+    <string name="openkeychain_required_long">%1$s 使用 &lt;b&gt;OpenKeychain&lt;/b&gt; 来加密和解密消息并管理公钥。&lt;br&gt;&lt;br&gt;它在 GPLv3+ 许可证下授权并可在 F-Droid 和 Google Play 上获得。&lt;br&gt;&lt;br&gt;&lt;small&gt;(请之后重新启动 %1$s。)&lt;/small&gt;</string>
+    <string name="restart">重新启动</string>
     <string name="install">安装</string>
-    <string name="openkeychain_not_installed">请安装OpenKeychain以解密</string>
-    <string name="offering">提供…</string>
-    <string name="waiting">等待…</string>
-    <string name="no_pgp_key">无OpenPGP密钥</string>
-    <string name="contact_has_no_pgp_key">因您的联系人未公布其公钥,无法加密您的信息。\n\n<small>请通知您的联系人设置OpenPGP。</small></string>
-    <string name="no_pgp_keys">无OpenPGP密钥</string>
-    <string name="contacts_have_no_pgp_keys">因您的联系人未公布其公钥,无法加密您的信息。\n\n<small>请通知您的联系人设置OpenPGP。</small></string>
+    <string name="openkeychain_not_installed">请安装 OpenKeychain</string>
+    <string name="offering">正在提供…</string>
+    <string name="waiting">正在等待…</string>
+    <string name="no_pgp_key">未找到 OpenPGP 密钥</string>
+    <string name="contact_has_no_pgp_key">无法加密消息,因为对方未公布其公钥。
+\n
+\n<small>请通知对方设置 OpenPGP。</small></string>
+    <string name="no_pgp_keys">未找到 OpenPGP 密钥</string>
+    <string name="contacts_have_no_pgp_keys">无法加密消息,因为对方未公布其公钥。
+\n
+\n<small>请通知对方设置 OpenPGP。</small></string>
     <string name="pref_general">常规</string>
     <string name="pref_accept_files">接收文件</string>
     <string name="pref_accept_files_summary">自动接收小于此大小的文件…</string>
@@ -117,125 +123,126 @@
     <string name="pref_notification_settings">通知</string>
     <string name="pref_vibrate">振动</string>
     <string name="pref_vibrate_summary">收到新消息时振动</string>
-    <string name="pref_led">通知灯</string>
+    <string name="pref_led">LED 通知</string>
     <string name="pref_led_summary">收到新消息时闪烁通知灯</string>
     <string name="pref_ringtone">铃声</string>
     <string name="pref_notification_sound">通知铃声</string>
-    <string name="pref_notification_sound_summary">新消息通知铃声</string>
-    <string name="pref_call_ringtone_summary">来电响铃</string>
-    <string name="pref_notification_grace_period">静默时间段</string>
-    <string name="pref_notification_grace_period_summary">在其他设备上检测到活动之后,通知在此时间段内将被静音。</string>
+    <string name="pref_notification_sound_summary">新消息的通知铃声</string>
+    <string name="pref_call_ringtone_summary">来电铃声</string>
+    <string name="pref_notification_grace_period">静默期</string>
+    <string name="pref_notification_grace_period_summary">在其他设备上检测到活动之后,通知在此期间静音。</string>
     <string name="pref_advanced_options">高级</string>
     <string name="pref_never_send_crash">从不发送崩溃报告</string>
-    <string name="pref_never_send_crash_summary">通过发送堆栈跟踪,您可以为开发提供帮助</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_prevent_screenshots_summary">在切换应用时隐藏应用内容并阻止屏幕截图</string>
     <string name="pref_ui_options">用户界面</string>
-    <string name="openpgp_error">OpenKeychain报告一个错误。</string>
-    <string name="bad_key_for_encryption">错误的加密密钥。</string>
+    <string name="openpgp_error">OpenKeychain 出现了错误。</string>
+    <string name="bad_key_for_encryption">加密密钥错误。</string>
     <string name="accept">接受</string>
-    <string name="error">产生了一个错误</string>
+    <string name="error">出现了错误</string>
     <string name="recording_error">错误</string>
-    <string name="your_account">你的账户</string>
+    <string name="your_account">您的账号</string>
     <string name="send_presence_updates">发送在线状态更新</string>
     <string name="receive_presence_updates">接收在线状态更新</string>
     <string name="ask_for_presence_updates">请求在线状态更新</string>
     <string name="attach_choose_picture">选择图片</string>
     <string name="attach_take_picture">拍摄图片</string>
     <string name="preemptively_grant">预先同意订阅请求</string>
-    <string name="error_not_an_image_file">您选择的文件不是图像</string>
-    <string name="error_compressing_image">无法转换图片</string>
-    <string name="error_file_not_found">未找到文件</string>
-    <string name="error_io_exception">常规I/O错误。可能是存储空间不足?</string>
-    <string name="error_security_exception_during_image_copy">你用来选择图片的应用没有提供读取文件的足够权限。
+    <string name="error_not_an_image_file">所选的文件不是图片</string>
+    <string name="error_compressing_image">无法转换图片文件</string>
+    <string name="error_file_not_found">文件未找到</string>
+    <string name="error_io_exception">常规 I/O 错误。也许存储空间已用完?</string>
+    <string name="error_security_exception_during_image_copy">用来选择图片的应用未提供读取文件的足够权限。
 \n
-\n<small>使用不同的文件管理器来选择图片</small>.</string>
-    <string name="error_security_exception">你用来共享此文件的应用程序没有提供足够的权限。</string>
+\n<small>请使用其他文件管理器选择图片</small>。</string>
+    <string name="error_security_exception">用来分享此文件的应用未提供足够的权限。</string>
     <string name="account_status_unknown">未知</string>
-    <string name="account_status_disabled">暂时不可用</string>
+    <string name="account_status_disabled">暂时禁用</string>
     <string name="account_status_online">在线</string>
-    <string name="account_status_connecting">连接中\u2026</string>
+    <string name="account_status_connecting">正在连接…</string>
     <string name="account_status_offline">离线</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_not_found">服务器未找到</string>
+    <string name="account_status_no_internet">未连接</string>
     <string name="account_status_regis_fail">注册失败</string>
-    <string name="account_status_regis_conflict"> 用户名已存在</string>
-    <string name="account_status_regis_success">注册完成</string>
+    <string name="account_status_regis_conflict">此用户名已在使用中</string>
+    <string name="account_status_regis_success">注册已完成</string>
     <string name="account_status_regis_not_sup">服务器不支持注册</string>
-    <string name="account_status_regis_invalid_token">无效的注册令牌</string>
-    <string name="account_status_tls_error">TLS协商失败</string>
-    <string name="account_status_tls_error_domain">域名不可验证</string>
-    <string name="account_status_policy_violation">违反政策</string>
+    <string name="account_status_regis_invalid_token">注册令牌无效</string>
+    <string name="account_status_tls_error">TLS 协商失败</string>
+    <string name="account_status_tls_error_domain">域名无法验证</string>
+    <string name="account_status_policy_violation">违反策略</string>
     <string name="account_status_incompatible_server">服务器不兼容</string>
-    <string name="account_status_incompatible_client">不兼容的客户端</string>
+    <string name="account_status_incompatible_client">客户端不兼容</string>
     <string name="account_status_stream_error">流错误</string>
     <string name="account_status_stream_opening_error">流打开错误</string>
     <string name="encryption_choice_unencrypted">TLS</string>
     <string name="encryption_choice_otr">OTR</string>
     <string name="encryption_choice_pgp">OpenPGP</string>
     <string name="encryption_choice_omemo">OMEMO</string>
-    <string name="mgmt_account_delete">删除账户</string>
-    <string name="mgmt_account_disable">暂时不可用</string>
+    <string name="mgmt_account_delete">删除账号</string>
+    <string name="mgmt_account_disable">暂时禁用</string>
     <string name="mgmt_account_publish_avatar">发布头像</string>
-    <string name="mgmt_account_publish_pgp">发布OpenPGP公钥</string>
-    <string name="unpublish_pgp">移除OpenPGP公钥</string>
-    <string name="unpublish_pgp_message">您确定要从在线状态中移除OpenPGP公钥吗?\n您的联系人将无法再向您发送 OpenPGP 加密信息。</string>
+    <string name="mgmt_account_publish_pgp">发布 OpenPGP 公钥</string>
+    <string name="unpublish_pgp">移除 OpenPGP 公钥</string>
+    <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="attach_record_voice">录制音频</string>
-    <string name="account_settings_jabber_id">XMPP地址</string>
-    <string name="block_jabber_id">拦截XMPP地址</string>
+    <string name="mgmt_account_enable">启用账号</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>
     <string name="account_settings_example_jabber_id">username@example.com</string>
     <string name="password">密码</string>
-    <string name="invalid_jid">这不是有效的XMPP地址</string>
-    <string name="error_out_of_memory">空间不足。图片过大</string>
-    <string name="add_phone_book_text">是否添加%s到通讯录?</string>
-    <string name="server_info_show_more">服务器属性</string>
+    <string name="invalid_jid">此 XMPP 地址无效</string>
+    <string name="error_out_of_memory">空间不足。图片太大</string>
+    <string name="add_phone_book_text">是否要添加 %s 到您的通讯录中?</string>
+    <string name="server_info_show_more">服务器信息</string>
     <string name="server_info_mam">XEP-0313:消息存档管理</string>
     <string name="server_info_carbon_messages">XEP-0280:消息抄送</string>
     <string name="server_info_csi">XEP-0352:客户端状态指示</string>
     <string name="server_info_blocking">XEP-0191:屏蔽指令</string>
-    <string name="server_info_roster_version">XEP-0237:通讯录版本管理</string>
+    <string name="server_info_roster_version">XEP-0237:花名册版本控制</string>
     <string name="server_info_stream_management">XEP-0198:流管理</string>
-    <string name="server_info_external_service_discovery">XEP-0215:发现外部服务</string>
-    <string name="server_info_pep">XEP-0163:个人事件协议(头像/OMEMO)</string>
-    <string name="server_info_http_upload">XEP-0363:HTTP文件上传</string>
+    <string name="server_info_external_service_discovery">XEP-0215:外部服务发现</string>
+    <string name="server_info_pep">XEP-0163:个人事件协议(头像/OMEMO)</string>
+    <string name="server_info_http_upload">XEP-0363:HTTP 文件上传</string>
     <string name="server_info_push">XEP-0357:推送</string>
     <string name="server_info_available">有效</string>
     <string name="server_info_unavailable">无效</string>
-    <string name="missing_public_keys">缺少公钥</string>
-    <string name="last_seen_now">刚来过</string>
-    <string name="last_seen_min">一分钟前来过</string>
-    <string name="last_seen_mins">%d分钟来过</string>
-    <string name="last_seen_hour">一小时前来过</string>
-    <string name="last_seen_hours">%d小时前来过</string>
-    <string name="last_seen_day">一天前来过</string>
-    <string name="last_seen_days">%d天前来过</string>
-    <string name="install_openkeychain">加密信息。请安装OpenKeychain以解密。</string>
-    <string name="openpgp_messages_found">发现新OpenPGP加密信息</string>
-    <string name="openpgp_key_id">OpenPGP密钥ID</string>
-    <string name="omemo_fingerprint">OMEMO指纹</string>
-    <string name="omemo_fingerprint_x509">v\\OMEMO指纹</string>
-    <string name="omemo_fingerprint_selected_message">OMEMO 指纹 (消息来源)</string>
-    <string name="omemo_fingerprint_x509_selected_message">v\\OMEMO 指纹 (消息来源)</string>
+    <string name="missing_public_keys">缺少公钥公布</string>
+    <string name="last_seen_now">最后上线于刚才</string>
+    <string name="last_seen_min">最后上线于 1 分钟前</string>
+    <string name="last_seen_mins">最后上线于 %d 分钟前</string>
+    <string name="last_seen_hour">最后上线于 1 小时前</string>
+    <string name="last_seen_hours">最后上线于 %d 小时前</string>
+    <string name="last_seen_day">最后上线于 1 天前</string>
+    <string name="last_seen_days">最后上线于 %d 天前</string>
+    <string name="install_openkeychain">加密消息。请安装 OpenKeychain 解密。</string>
+    <string name="openpgp_messages_found">发现新的 OpenPGP 加密消息</string>
+    <string name="openpgp_key_id">OpenPGP 密钥 ID</string>
+    <string name="omemo_fingerprint">OMEMO 指纹</string>
+    <string name="omemo_fingerprint_x509">v\\OMEMO 指纹</string>
+    <string name="omemo_fingerprint_selected_message">OMEMO 指纹(消息来源)</string>
+    <string name="omemo_fingerprint_x509_selected_message">v\\OMEMO 指纹(消息来源)</string>
     <string name="other_devices">其他设备</string>
-    <string name="trust_omemo_fingerprints">信任的OMEMO指纹</string>
-    <string name="fetching_keys">获取密钥中…</string>
+    <string name="trust_omemo_fingerprints">信任 OMEMO 指纹</string>
+    <string name="fetching_keys">正在获取密钥…</string>
     <string name="done">完成</string>
     <string name="decrypt">解密</string>
     <string name="search">搜索</string>
     <string name="enter_contact">输入联系人</string>
     <string name="delete_contact">删除联系人</string>
-    <string name="view_contact_details">查看联系人详细信息</string>
-    <string name="block_contact">封禁联系人</string>
-    <string name="unblock_contact">解封联系人</string>
+    <string name="view_contact_details">查看联系人详情</string>
+    <string name="block_contact">屏蔽联系人</string>
+    <string name="unblock_contact">解除屏蔽联系人</string>
     <string name="create">新建</string>
     <string name="select">选择</string>
-    <string name="contact_already_exists">联系人已存在</string>
+    <string name="contact_already_exists">此联系人已存在</string>
     <string name="join">加入</string>
     <string name="channel_full_jid_example">channel@conference.example.com/nick</string>
     <string name="channel_bare_jid_example">channel@conference.example.com</string>
@@ -243,469 +250,483 @@
     <string name="delete_bookmark">删除书签</string>
     <string name="destroy_room">解散群聊</string>
     <string name="destroy_channel">解散频道</string>
-    <string name="destroy_room_dialog">您确定要解散此群聊吗?\n\n<b>警告:</b>此群聊将在服务器上完全删除。</string>
-    <string name="destroy_channel_dialog">您确定要解散此公共频道吗?\n\n<b>警告:</b>该频道将在服务器上完全删除。</string>
+    <string name="destroy_room_dialog">是否确定要解散此群聊?
+\n
+\n<b>警告:</b>将在服务器上完全移除群聊。</string>
+    <string name="destroy_channel_dialog">是否确定要解散此公开频道?
+\n
+\n<b>警告:</b>将在服务器上完全移除频道。</string>
     <string name="could_not_destroy_room">无法解散群聊</string>
     <string name="could_not_destroy_channel">无法解散频道</string>
-    <string name="action_edit_subject">编辑群聊主题</string>
-    <string name="topic">主题</string>
+    <string name="action_edit_subject">编辑群聊话题</string>
+    <string name="topic">话题</string>
     <string name="joining_conference">正在加入群聊…</string>
     <string name="leave">离开</string>
-    <string name="contact_added_you">联系人已添加你到通讯录</string>
-    <string name="add_back">反向添加</string>
-    <string name="contact_has_read_up_to_this_point">%s读到这里了</string>
-    <string name="contacts_have_read_up_to_this_point">%s读到这里了</string>
-    <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s和另外%2$d人读到这里了</string>
-    <string name="everyone_has_read_up_to_this_point">所有人都读到这里了</string>
+    <string name="contact_added_you">对方已将您添加到联系人列表</string>
+    <string name="add_back">添加对方</string>
+    <string name="contact_has_read_up_to_this_point">%s 已阅读至此</string>
+    <string name="contacts_have_read_up_to_this_point">%s 已阅读至此</string>
+    <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s 和其他 %2$d 人已阅读至此</string>
+    <string name="everyone_has_read_up_to_this_point">每个人都已阅读至此</string>
     <string name="publish">发布</string>
-    <string name="touch_to_choose_picture">点击头像以选择图片</string>
+    <string name="touch_to_choose_picture">点击头像即可从图库中选择图片</string>
     <string name="publishing">正在发布…</string>
-    <string name="error_publish_avatar_server_reject">服务器拒绝了您的发布请求</string>
+    <string name="error_publish_avatar_server_reject">服务器拒绝了您的发布</string>
     <string name="error_publish_avatar_converting">无法转换图片</string>
-    <string name="error_saving_avatar">不能将头像保存至磁盘</string>
-    <string name="or_long_press_for_default">(长按以恢复默认)</string>
-    <string name="error_publish_avatar_no_server_support">服务器不支持头像</string>
+    <string name="error_saving_avatar">无法将头像保存到磁盘</string>
+    <string name="or_long_press_for_default">(或长按恢复默认)</string>
+    <string name="error_publish_avatar_no_server_support">服务器不支持发布头像</string>
     <string name="private_message">私聊</string>
-    <string name="private_message_to">至%s</string>
-    <string name="send_private_message_to">与%s私聊</string>
+    <string name="private_message_to">至 %s</string>
+    <string name="send_private_message_to">发送私信至 %s</string>
     <string name="connect">连接</string>
-    <string name="account_already_exists">该账户已存在</string>
+    <string name="account_already_exists">此账号已存在</string>
     <string name="next">下一步</string>
     <string name="server_info_session_established">会话已建立</string>
     <string name="skip">跳过</string>
-    <string name="disable_notifications">关闭通知</string>
+    <string name="disable_notifications">禁用通知</string>
     <string name="enable">启用</string>
-    <string name="conference_requires_password">需要密码才能进入该群聊</string>
+    <string name="conference_requires_password">需要密码才能进入此群聊</string>
     <string name="enter_password">输入密码</string>
-    <string name="request_presence_updates">请先请求联系人在线状态更新。
+    <string name="request_presence_updates">请先向对方请求在线状态更新。
 \n
-\n<small>这将被用来判断您的联系人正在使用的聊天应用</small>。</string>
-    <string name="request_now">现在请求</string>
+\n<small>这将用于确定对方正在使用的聊天应用</small>。</string>
+    <string name="request_now">立即请求</string>
     <string name="ignore">忽略</string>
-    <string name="without_mutual_presence_updates"><b>警告:</b>在没有相互更新在线状态的情况下发送将会出现未知问题。\n\n<small>前往联系人详情以验证您订阅的在线状态。</small></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_summary">允许对方发送后编辑信息</string>
-    <string name="pref_expert_options">高级设置</string>
-    <string name="pref_expert_options_summary">请谨慎使用</string>
-    <string name="title_activity_about_x">关于%s</string>
-    <string name="title_pref_quiet_hours">静默时间段</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>
+    <string name="title_activity_about_x">关于 %s</string>
+    <string name="title_pref_quiet_hours">免打扰时间</string>
     <string name="title_pref_quiet_hours_start_time">开始时间</string>
     <string name="title_pref_quiet_hours_end_time">结束时间</string>
-    <string name="title_pref_enable_quiet_hours">启用静默时间段</string>
-    <string name="pref_quiet_hours_summary">在静默时间段内通知将保持静音</string>
+    <string name="title_pref_enable_quiet_hours">启用免打扰时间</string>
+    <string name="pref_quiet_hours_summary">通知将在免打扰时间内静音</string>
     <string name="pref_expert_options_other">其他</string>
     <string name="pref_autojoin">同步书签</string>
-    <string name="pref_autojoin_summary">加入或离开多用户聊天时设置 “autojoin\" 标志,并回应其他客户端所做更改。</string>
-    <string name="toast_message_omemo_fingerprint">OMEMO指纹已拷贝到剪贴板</string>
-    <string name="conference_banned">您被封禁了</string>
-    <string name="conference_members_only">这个群聊只允许成员聊天</string>
+    <string name="pref_autojoin_summary">在进入或离开多用户聊天时设置“自动加入”标志,并回应其他客户端所做的改变。</string>
+    <string name="toast_message_omemo_fingerprint">OMEMO 指纹已复制到剪贴板</string>
+    <string name="conference_banned">此群聊已将您封禁了</string>
+    <string name="conference_members_only">此群聊仅限成员参与</string>
     <string name="conference_resource_constraint">资源限制</string>
-    <string name="conference_kicked">您被从此群聊踢出</string>
-    <string name="conference_shutdown">这个群聊已被关闭</string>
-    <string name="conference_unknown_error">您已不在该群组</string>
-    <string name="conference_technical_problems">你出于技术原因离开了群聊</string>
-    <string name="using_account">使用账户%s</string>
-    <string name="hosted_on">托管于%s</string>
-    <string name="checking_x">正在HTTP服务器中检查%s</string>
-    <string name="not_connected_try_again">未连接。请稍后重试</string>
-    <string name="check_x_filesize">检查%s的大小</string>
-    <string name="check_x_filesize_on_host">在%2$s上检查%1$s的大小</string>
+    <string name="conference_kicked">此群聊已将您踢出了</string>
+    <string name="conference_shutdown">此群聊已关闭</string>
+    <string name="conference_unknown_error">您已不在群聊中</string>
+    <string name="conference_technical_problems">由于技术原因,您离开了群聊</string>
+    <string name="using_account">正在使用账号 %s</string>
+    <string name="hosted_on">托管于 %s</string>
+    <string name="checking_x">正在 HTTP 主机上检查 %s</string>
+    <string name="not_connected_try_again">您未连接。稍后再试</string>
+    <string name="check_x_filesize">检查 %s 的大小</string>
+    <string name="check_x_filesize_on_host">在 %2$s 上检查 %1$s 的大小</string>
     <string name="message_options">消息选项</string>
     <string name="quote">引用</string>
-    <string name="paste_as_quote">作为引用粘贴</string>
-    <string name="copy_original_url">复制原始URL</string>
-    <string name="send_again">重新发送</string>
-    <string name="file_url">文件URL</string>
-    <string name="url_copied_to_clipboard">已经拷贝URL到剪贴板</string>
-    <string name="jabber_id_copied_to_clipboard">已复制XMPP地址到剪贴板</string>
-    <string name="error_message_copied_to_clipboard">已复制错误信息到剪贴板</string>
-    <string name="web_address">web地址</string>
+    <string name="paste_as_quote">粘贴为引用</string>
+    <string name="copy_original_url">复制原始 URL</string>
+    <string name="send_again">再次发送</string>
+    <string name="file_url">文件 URL</string>
+    <string name="url_copied_to_clipboard">已复制 URL 到剪贴板</string>
+    <string name="jabber_id_copied_to_clipboard">已复制 XMPP 地址到剪贴板</string>
+    <string name="error_message_copied_to_clipboard">已复制错误消息到剪贴板</string>
+    <string name="web_address">web 地址</string>
     <string name="scan_qr_code">扫描二维码</string>
     <string name="show_qr_code">显示二维码</string>
-    <string name="show_block_list">显示封禁列表</string>
-    <string name="account_details">账户详情</string>
+    <string name="show_block_list">显示屏蔽列表</string>
+    <string name="account_details">账号详情</string>
     <string name="confirm">确认</string>
-    <string name="try_again">重试</string>
+    <string name="try_again">再试一次</string>
     <string name="pref_keep_foreground_service">前台服务</string>
     <string name="pref_keep_foreground_service_summary">防止操作系统中断连接</string>
     <string name="pref_create_backup">创建备份</string>
-    <string name="pref_create_backup_summary">备份文件将保存在%s</string>
-    <string name="notification_create_backup_title">正在备份文件</string>
-    <string name="notification_backup_created_title">备份已创建</string>
-    <string name="notification_backup_created_subtitle">此备份文件已经存储在%s</string>
+    <string name="pref_create_backup_summary">备份文件将存储在 %s</string>
+    <string name="notification_create_backup_title">正在创建备份文件</string>
+    <string name="notification_backup_created_title">已创建备份</string>
+    <string name="notification_backup_created_subtitle">此备份文件已存储在 %s</string>
     <string name="restoring_backup">正在恢复备份</string>
-    <string name="notification_restored_backup_title">备份已恢复</string>
-    <string name="notification_restored_backup_subtitle">别忘了启用帐号。</string>
+    <string name="notification_restored_backup_title">已恢复备份</string>
+    <string name="notification_restored_backup_subtitle">不要忘记启用账号。</string>
     <string name="choose_file">选择文件</string>
-    <string name="receiving_x_file">正在下载%1$s(已完成%2$d%%)</string>
-    <string name="download_x_file">下载%s</string>
-    <string name="delete_x_file">删除%s</string>
+    <string name="receiving_x_file">正在接收 %1$s (%2$d%% 已完成)</string>
+    <string name="download_x_file">下载 %s</string>
+    <string name="delete_x_file">删除 %s</string>
     <string name="file">文件</string>
-    <string name="open_x_file"> 打开%s</string>
-    <string name="sending_file">正在发送(已完成%1$d%%)</string>
-    <string name="preparing_file">准备传输文件</string>
-    <string name="x_file_offered_for_download">可以下载%s</string>
+    <string name="open_x_file">打开 %s</string>
+    <string name="sending_file">正在发送 (%1$d%% 已完成)</string>
+    <string name="preparing_file">正在准备分享文件</string>
+    <string name="x_file_offered_for_download">%s 可供下载</string>
     <string name="cancel_transmission">取消传输</string>
-    <string name="file_transmission_failed">文件传输失败</string>
+    <string name="file_transmission_failed">无法分享文件</string>
     <string name="file_transmission_cancelled">文件传输已取消</string>
-    <string name="file_deleted">文件已经删除</string>
-    <string name="no_application_found_to_open_file">没有可以打开此文件的应用</string>
-    <string name="no_application_found_to_open_link">没有可以打开此链接的应用</string>
+    <string name="file_deleted">文件已删除</string>
+    <string name="no_application_found_to_open_file">未找到可以打开文件的应用</string>
+    <string name="no_application_found_to_open_link">未找到可以打开链接的应用</string>
     <string name="no_application_found_to_view_contact">未找到可以查看联系人的应用</string>
     <string name="pref_show_dynamic_tags">动态标签</string>
     <string name="pref_show_dynamic_tags_summary">在联系人下方显示只读标签</string>
     <string name="enable_notifications">启用通知</string>
-    <string name="no_conference_server_found">未找到群聊服务器</string>
-    <string name="conference_creation_failed">群聊创建失败</string>
-    <string name="account_image_description">账户头像</string>
-    <string name="copy_omemo_clipboard_description">复制OMEMO指纹到剪贴板</string>
-    <string name="regenerate_omemo_key">重新生成OMEMO密钥</string>
+    <string name="no_conference_server_found">群聊服务器未找到</string>
+    <string name="conference_creation_failed">无法创建群聊</string>
+    <string name="account_image_description">账号头像</string>
+    <string name="copy_omemo_clipboard_description">复制 OMEMO 指纹到剪贴板</string>
+    <string name="regenerate_omemo_key">重新生成 OMEMO 密钥</string>
     <string name="clear_other_devices">清除设备</string>
-    <string name="clear_other_devices_desc">清除所有其他设备的OMEMO通告?下次设备连接时将重新通告,但可能收不到你发送的消息。</string>
-    <string name="error_no_keys_to_trust_server_error">此联系人没有可用的密钥。
-\n无法从服务器获取新密钥。也许你的联系人所在服务器发生问题了?</string>
-    <string name="error_no_keys_to_trust_presence">没有可以用于这个账户的密钥。\n请确保你有相互的在线状态的订阅。</string>
-    <string name="error_trustkeys_title">出错了</string>
+    <string name="clear_other_devices_desc">是否确定要从 OMEMO 公布中清除所有其他设备?下次连接时,设备将会重新公布,但可能不会收到在此期间发送的消息。</string>
+    <string name="error_no_keys_to_trust_server_error">对方没有可用的密钥。
+\n无法从服务器获取新密钥。也许是对方的服务器出了问题?</string>
+    <string name="error_no_keys_to_trust_presence">对方没有可用的密钥。
+\n请确保双方都有在线状态订阅。</string>
+    <string name="error_trustkeys_title">出了点问题</string>
     <string name="fetching_history_from_server">正在从服务器获取历史记录</string>
     <string name="no_more_history_on_server">服务器上没有更多历史记录</string>
-    <string name="updating">更新中…</string>
+    <string name="updating">正在更新…</string>
     <string name="password_changed">密码已修改!</string>
-    <string name="could_not_change_password">不能修改密码</string>
+    <string name="could_not_change_password">无法修改密码</string>
     <string name="change_password">修改密码</string>
     <string name="current_password">当前密码</string>
     <string name="new_password">新密码</string>
     <string name="password_should_not_be_empty">密码不能为空</string>
-    <string name="enable_all_accounts">启用所有账户</string>
-    <string name="disable_all_accounts">禁用所有账户</string>
-    <string name="perform_action_with">选择一个操作</string>
-    <string name="no_affiliation">没有从属关系</string>
+    <string name="enable_all_accounts">启用所有账号</string>
+    <string name="disable_all_accounts">禁用所有账号</string>
+    <string name="perform_action_with">执行操作</string>
+    <string name="no_affiliation">无从属关系</string>
     <string name="no_role">离线</string>
-    <string name="outcast">已封禁</string>
+    <string name="outcast">被驱逐者</string>
     <string name="member">成员</string>
     <string name="advanced_mode">高级模式</string>
     <string name="grant_membership">授予成员权限</string>
-    <string name="remove_membership">吊销成员权限</string>
+    <string name="remove_membership">撤销成员权限</string>
     <string name="grant_admin_privileges">授予管理员权限</string>
-    <string name="remove_admin_privileges">吊销管理员权限</string>
+    <string name="remove_admin_privileges">撤销管理员权限</string>
     <string name="grant_owner_privileges">授予所有者权限</string>
-    <string name="remove_owner_privileges">吊销所有者权限</string>
+    <string name="remove_owner_privileges">撤销所有者权限</string>
     <string name="remove_from_room">从群聊中移除</string>
     <string name="remove_from_channel">从频道中移除</string>
-    <string name="could_not_change_affiliation">不能修改%s的从属关系</string>
+    <string name="could_not_change_affiliation">无法更改 %s 的从属关系</string>
     <string name="ban_from_conference">从群聊中封禁</string>
     <string name="ban_from_channel">从频道中封禁</string>
-    <string name="removing_from_public_conference">%s将被从公共频道中移除。只有将此用户封禁才能将他永远移除。</string>
-    <string name="ban_now">立刻封禁</string>
-    <string name="could_not_change_role">不能修改%s的角色</string>
-    <string name="conference_options">私密群聊设置</string>
-    <string name="channel_options">公开频道设置</string>
-    <string name="members_only">私密,只有成员可以加入</string>
-    <string name="non_anonymous">使XMPP地址对所有人可见</string>
-    <string name="moderated">使频道受到管理</string>
-    <string name="you_are_not_participating">您尚未参与</string>
-    <string name="modified_conference_options">群聊设置修改成功!</string>
-    <string name="could_not_modify_conference_options">无法更改群聊设置</string>
+    <string name="removing_from_public_conference">您正试图从公开频道中移除 %s。唯一的办法就是永远封禁此用户。</string>
+    <string name="ban_now">立即封禁</string>
+    <string name="could_not_change_role">无法更改 %s 的角色</string>
+    <string name="conference_options">私人群聊配置</string>
+    <string name="channel_options">公开频道配置</string>
+    <string name="members_only">私人,仅限成员</string>
+    <string name="non_anonymous">使 XMPP 地址对任何人可见</string>
+    <string name="moderated">对频道进行审核</string>
+    <string name="you_are_not_participating">您未参与</string>
+    <string name="modified_conference_options">群聊选项修改成功!</string>
+    <string name="could_not_modify_conference_options">无法修改群聊选项</string>
     <string name="never">从不</string>
     <string name="until_further_notice">直至另行通知</string>
-    <string name="snooze">小睡</string>
+    <string name="snooze">稍后提醒</string>
     <string name="reply">回复</string>
     <string name="mark_as_read">标记为已读</string>
     <string name="pref_input_options">输入</string>
-    <string name="pref_enter_is_send">点击回车发送</string>
-    <string name="pref_enter_is_send_summary">使用Enter键发送消息。即使禁用此选项,也可以使用Ctrl+Enter发送消息。</string>
-    <string name="pref_display_enter_key">显示回车键</string>
-    <string name="pref_display_enter_key_summary">将表情键改为回车键</string>
+    <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="audio">音频</string>
     <string name="video">视频</string>
     <string name="image">图片</string>
-    <string name="vector_graphic">矢量图</string>
+    <string name="vector_graphic">矢量图形</string>
     <string name="multimedia_file">多媒体文件</string>
-    <string name="pdf_document">PDF文档</string>
-    <string name="apk">Android App</string>
+    <string name="pdf_document">PDF 文档</string>
+    <string name="apk">Android 应用</string>
     <string name="vcard">联系人</string>
-    <string name="avatar_has_been_published">头像已经发布!</string>
-    <string name="sending_x_file">正在发送%s</string>
-    <string name="offering_x_file">正在提供%s</string>
-    <string name="hide_offline">隐藏离线联系人</string>
-    <string name="contact_is_typing">%s正在输入……</string>
-    <string name="contact_has_stopped_typing">%s已停止输入</string>
-    <string name="contacts_are_typing">%s正在输入……</string>
-    <string name="contacts_have_stopped_typing">%s已停止输入</string>
+    <string name="avatar_has_been_published">已发布头像!</string>
+    <string name="sending_x_file">正在发送 %s</string>
+    <string name="offering_x_file">正在提供 %s</string>
+    <string name="hide_offline">隐藏离线</string>
+    <string name="contact_is_typing">%s 正在输入…</string>
+    <string name="contact_has_stopped_typing">%s 已停止输入</string>
+    <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>
+    <string name="no_application_found_to_display_location">未找到可以显示位置的应用</string>
     <string name="location">位置</string>
-    <string name="title_undo_swipe_out_conversation">聊天已关闭</string>
-    <string name="title_undo_swipe_out_group_chat">离开私密群聊</string>
+    <string name="title_undo_swipe_out_conversation">对话已关闭</string>
+    <string name="title_undo_swipe_out_group_chat">离开私人群聊</string>
     <string name="title_undo_swipe_out_channel">离开公开频道</string>
-    <string name="pref_dont_trust_system_cas_title">不信任系统CA</string>
-    <string name="pref_dont_trust_system_cas_summary">所有证书必须手动通过</string>
+    <string name="pref_dont_trust_system_cas_title">不信任系统证书颁发机构</string>
+    <string name="pref_dont_trust_system_cas_summary">所有证书必须手动批准</string>
     <string name="pref_remove_trusted_certificates_title">移除证书</string>
-    <string name="pref_remove_trusted_certificates_summary">删除手动通过的证书</string>
-    <string name="toast_no_trusted_certs">没有手动通过的证书</string>
+    <string name="pref_remove_trusted_certificates_summary">删除手动批准的证书</string>
+    <string name="toast_no_trusted_certs">没有手动批准的证书</string>
     <string name="dialog_manage_certs_title">移除证书</string>
-    <string name="dialog_manage_certs_positivebutton">删除已选</string>
+    <string name="dialog_manage_certs_positivebutton">删除所选</string>
     <string name="dialog_manage_certs_negativebutton">取消</string>
     <plurals name="toast_delete_certificates">
-        <item quantity="other">%d个证书已被删除</item>
+        <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>
+    <string name="recently_used">最近使用</string>
     <string name="choose_quick_action">选择快捷操作</string>
     <string name="search_contacts">搜索联系人</string>
-    <string name="send_private_message">发送私密消息</string>
-    <string name="user_has_left_conference">%1$s离开了群聊</string>
+    <string name="send_private_message">发送私信</string>
+    <string name="user_has_left_conference">%1$s 离开了群聊</string>
     <string name="username">用户名</string>
     <string name="username_hint">用户名</string>
-    <string name="invalid_username">该用户名无效</string>
-    <string name="download_failed_server_not_found">下载失败:未找到服务器</string>
-    <string name="download_failed_file_not_found">下载失败:未找到文件</string>
-    <string name="download_failed_could_not_connect">下载失败:无法连接到服务器</string>
-    <string name="download_failed_could_not_write_file">下载失败:不能写入文件</string>
-    <string name="download_failed_invalid_file">下载失败:无效文件</string>
-    <string name="account_status_tor_unavailable">Tor网络不可用</string>
+    <string name="invalid_username">此用户名无效</string>
+    <string name="download_failed_server_not_found">下载失败:服务器未找到</string>
+    <string name="download_failed_file_not_found">下载失败:文件未找到</string>
+    <string name="download_failed_could_not_connect">下载失败:无法连接到主机</string>
+    <string name="download_failed_could_not_write_file">下载失败:无法写入文件</string>
+    <string name="download_failed_invalid_file">下载失败:文件无效</string>
+    <string name="account_status_tor_unavailable">Tor 网络不可用</string>
     <string name="account_status_bind_failure">绑定失败</string>
     <string name="account_status_host_unknown">服务器不能为域名做出响应</string>
     <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_dnd_on_silent_mode">在静音模式显示为忙碌</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_presence_settings">在线状态</string>
+    <string name="pref_away_when_screen_off">设备锁定时离开</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_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="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>
-    <string name="jid_does_not_match_certificate">XMPP地址与证书不匹配</string>
-    <string name="action_renew_certificate">更新证书</string>
-    <string name="error_fetching_omemo_key">获取OMEMO密钥时发生错误!</string>
-    <string name="verified_omemo_key_with_certificate">请用证书验证OMEMO密钥!</string>
-    <string name="device_does_not_support_certificates">您的设备不支持客户端证书选择!</string>
+    <string name="captcha_hint">输入上图中的文字</string>
+    <string name="certificate_chain_is_not_trusted">不受信任的证书链</string>
+    <string name="jid_does_not_match_certificate">XMPP 地址与证书不匹配</string>
+    <string name="action_renew_certificate">续订证书</string>
+    <string name="error_fetching_omemo_key">获取 OMEMO 密钥时出错!</string>
+    <string name="verified_omemo_key_with_certificate">已通过证书验证 OMEMO 密钥!</string>
+    <string name="device_does_not_support_certificates">设备不支持客户端证书选择!</string>
     <string name="pref_connection_options">连接</string>
-    <string name="pref_use_tor">通过Tor连接</string>
-    <string name="pref_use_tor_summary">所有连接使用Tor网络传输,需要Orbot</string>
-    <string name="account_settings_hostname">服务器名</string>
+    <string name="pref_use_tor">通过 Tor 连接</string>
+    <string name="pref_use_tor_summary">通过 Tor 网络传输所有连接。需要 Orbot</string>
+    <string name="account_settings_hostname">主机名</string>
     <string name="account_settings_port">端口</string>
-    <string name="hostname_or_onion">服务器或者.onion地址</string>
-    <string name="not_a_valid_port">该端口号无效</string>
-    <string name="not_valid_hostname">该主机名无效</string>
-    <string name="connected_accounts">已连接%2$d个中的%1$d个账户</string>
+    <string name="hostname_or_onion">服务器或 .onion 地址</string>
+    <string name="not_a_valid_port">此端口号无效</string>
+    <string name="not_valid_hostname">此主机名无效</string>
+    <string name="connected_accounts">已连接 %2$d 个账号中的 %1$d 个</string>
     <plurals name="x_messages">
-        <item quantity="other">%d条消息</item>
+        <item quantity="other">%d 条消息</item>
     </plurals>
     <string name="load_more_messages">加载更多消息</string>
-    <string name="shared_file_with_x">文件已分享给%s</string>
-    <string name="shared_image_with_x">图片已分享给%s</string>
-    <string name="shared_images_with_x">图片已分享给%s</string>
-    <string name="shared_text_with_x">文本已分享给%s</string>
+    <string name="shared_file_with_x">文件已分享给 %s</string>
+    <string name="shared_image_with_x">图片已分享给 %s</string>
+    <string name="shared_images_with_x">图片已分享给 %s</string>
+    <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_long">%1$s想要访问通讯录的权限来将它与你的 XMPP 联系人列表相匹配。\n这会显示你的联系人的完整姓名和头像。\n\n%1$s只会读取你的通讯录并在本地进行匹配,不会将信息上传到你的服务器。</string>
-    <string name="notify_on_all_messages">为所有信息显示通知</string>
-    <string name="notify_only_when_highlighted">只在被提到时通知</string>
+    <string name="no_camera_permission">授予 %1$s 访问相机的权限</string>
+    <string name="sync_with_contacts">与联系人同步</string>
+    <string name="sync_with_contacts_long">%1$s 想要访问通讯录权限来将它与 XMPP 联系人列表相匹配。
+\n这将会显示联系人的全名和头像。
+\n
+\n%1$s 将只会读取通讯录并在本地匹配,不会上传任何东西到服务器。</string>
+    <string name="notify_on_all_messages">通知所有消息</string>
+    <string name="notify_only_when_highlighted">仅在提及时通知</string>
     <string name="notify_never">通知已禁用</string>
     <string name="notify_paused">通知已暂停</string>
-    <string name="pref_picture_compression">图像压缩</string>
-    <string name="pref_picture_compression_summary">提示:使用“选择文件”发送原图。这将忽略此设置。</string>
-    <string name="always">总是</string>
-    <string name="large_images_only">仅大图片</string>
-    <string name="battery_optimizations_enabled">已启用节电模式</string>
-    <string name="battery_optimizations_enabled_explained">你的设备正对 %1$s 实施强力电池优化,这可能导致通知延迟甚至消息丢失。\n建议禁用这些优化。</string>
-    <string name="battery_optimizations_enabled_dialog">你的设备正对 %1$s 实施强力电池优化,这可能导致通知延迟甚至消息丢失。
+    <string name="pref_picture_compression">图片压缩</string>
+    <string name="pref_picture_compression_summary">提示:无论此设置如何,使用“选择文件”会发送未压缩的图片。</string>
+    <string name="always">始终</string>
+    <string name="large_images_only">仅限大图片</string>
+    <string name="battery_optimizations_enabled">电池优化已启用</string>
+    <string name="battery_optimizations_enabled_explained">设备正对 %1$s 实施强力电池优化,这可能会导致通知延迟甚至消息丢失。
+\n建议禁用它们。</string>
+    <string name="battery_optimizations_enabled_dialog">设备正对 %1$s 实施强力电池优化,这可能会导致通知延迟甚至消息丢失。
 \n
-\n你将被请求禁用这些优化。</string>
+\n现在将要求您禁用它们。</string>
     <string name="disable">禁用</string>
-    <string name="selection_too_large">选择区域过大</string>
-    <string name="no_accounts">(没有启用的账户)</string>
-    <string name="this_field_is_required">必填</string>
+    <string name="selection_too_large">所选区域太大</string>
+    <string name="no_accounts">(没有启用的账号)</string>
+    <string name="this_field_is_required">此字段是必需的</string>
     <string name="correct_message">更正消息</string>
     <string name="send_corrected_message">发送更正后的消息</string>
-    <string name="no_keys_just_confirm">您已经验证了该用户。点击“完成”让%s加入群聊。 </string>
-    <string name="this_account_is_disabled">你已经禁用了此账户</string>
+    <string name="no_keys_just_confirm">您已经安全地验证了此用户的指纹以确认信任。选择“完成”以确认 %s 加入群聊。</string>
+    <string name="this_account_is_disabled">您已禁用了此账号</string>
     <string name="security_error_invalid_file_access">安全错误:文件访问无效!</string>
-    <string name="no_application_to_share_uri">未找到可以分享此链接的应用</string>
-    <string name="share_uri_with">分享链接……</string>
+    <string name="no_application_to_share_uri">未找到可以分享 URI 的应用</string>
+    <string name="share_uri_with">分享 URI 至…</string>
     <string name="agree_and_continue">同意并继续</string>
-    <string name="magic_create_text">此向导将为您在conversations.im 上创建一个账户。
-\n您的联系人可以通过您的XMPP完整地址与您聊天。</string>
-    <string name="your_full_jid_will_be">您的XMPP完整地址将是:%s</string>
-    <string name="create_account">创建账户</string>
-    <string name="use_own_provider">使用我自己的服务器</string>
-    <string name="pick_your_username">输入您的用户名</string>
+    <string name="magic_create_text">指导您在 conversations.im 上创建账号。
+\n当选择 conversations.im 作为提供者时,向其他 XMPP 用户提供您的完整地址,就能和对方交流。</string>
+    <string name="your_full_jid_will_be">您的完整 XMPP 地址将是:%s</string>
+    <string name="create_account">创建账号</string>
+    <string name="use_own_provider">使用我自己的提供者</string>
+    <string name="pick_your_username">选择您的用户名</string>
     <string name="pref_manually_change_presence">手动更改在线状态</string>
-    <string name="pref_manually_change_presence_summary">编辑状态信息时设置您是否有空。</string>
+    <string name="pref_manually_change_presence_summary">在编辑状态信息时,让您的联系人知道您是否可以聊天。</string>
     <string name="status_message">状态信息</string>
     <string name="presence_chat">有空聊天</string>
     <string name="presence_online">在线</string>
     <string name="presence_away">离开</string>
-    <string name="presence_xa">没时间</string>
+    <string name="presence_xa">没空</string>
     <string name="presence_dnd">忙碌</string>
-    <string name="secure_password_generated">安全密码已生成</string>
-    <string name="device_does_not_support_battery_op">该设备不支持禁用电池优化</string>
-    <string name="registration_please_wait">注册失败:请稍后重试</string>
+    <string name="secure_password_generated">已生成安全密码</string>
+    <string name="device_does_not_support_battery_op">设备不支持退出电池优化</string>
+    <string name="registration_please_wait">注册失败:稍后再试</string>
     <string name="registration_password_too_weak">注册失败:密码太弱</string>
-    <string name="choose_participants">选择成员</string>
+    <string name="choose_participants">选择参与者</string>
     <string name="creating_conference">正在创建群聊…</string>
-    <string name="invite_again">重新邀请</string>
+    <string name="invite_again">再次邀请</string>
     <string name="gp_disable">禁用</string>
     <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">让您的联系人知道您最后使用 Conversations 的时间</string>
     <string name="pref_privacy">隐私</string>
     <string name="pref_theme_options">主题</string>
-    <string name="pref_theme_options_summary">选择主题色彩</string>
+    <string name="pref_theme_options_summary">选择主题颜色</string>
     <string name="pref_theme_automatic">自动</string>
-    <string name="pref_theme_light">明亮</string>
-    <string name="pref_theme_dark">灰暗</string>
-    <string name="unable_to_connect_to_keychain">无法连接到OpenKeychain</string>
-    <string name="this_device_is_no_longer_in_use">不再使用此设备</string>
+    <string name="pref_theme_light">浅色</string>
+    <string name="pref_theme_dark">深色</string>
+    <string name="unable_to_connect_to_keychain">无法连接到 OpenKeychain</string>
+    <string name="this_device_is_no_longer_in_use">此设备已不再使用</string>
     <string name="type_pc">电脑</string>
     <string name="type_phone">手机</string>
     <string name="type_tablet">平板</string>
-    <string name="type_web">浏览器</string>
+    <string name="type_web">Web 浏览器</string>
     <string name="type_console">控制台</string>
     <string name="payment_required">需要付款</string>
-    <string name="missing_internet_permission">允许联网</string>
+    <string name="missing_internet_permission">授予使用互联网的权限</string>
     <string name="me">我</string>
-    <string name="contact_asks_for_presence_subscription">联系人请求在线状态订阅</string>
+    <string name="contact_asks_for_presence_subscription">对方请求在线状态订阅</string>
     <string name="allow">允许</string>
-    <string name="no_permission_to_access_x">无权访问%s</string>
-    <string name="remote_server_not_found">找不到远程服务器</string>
+    <string name="no_permission_to_access_x">没有权限访问 %s</string>
+    <string name="remote_server_not_found">远程服务器未找到</string>
     <string name="remote_server_timeout">远程服务器超时</string>
-    <string name="unable_to_update_account">无法更新账户</string>
-    <string name="report_jid_as_spammer">举报此 XMPP 地址发送垃圾信息。</string>
-    <string name="pref_delete_omemo_identities">删除OMEMO身份</string>
-    <string name="pref_delete_omemo_identities_summary">重新生成OMEMO密钥。所有联系人都需要再次认证。请将此作为最后的办法。</string>
-    <string name="delete_selected_keys">删除选择的密钥</string>
-    <string name="error_publish_avatar_offline">你需要连接才能发布头像。</string>
+    <string name="unable_to_update_account">无法更新账号</string>
+    <string name="report_jid_as_spammer">举报此 XMPP 地址发送垃圾消息。</string>
+    <string name="pref_delete_omemo_identities">删除 OMEMO 身份</string>
+    <string name="pref_delete_omemo_identities_summary">重新生成 OMEMO 密钥。您的所有联系人将必须再次验证您。仅将此作为最后的方法。</string>
+    <string name="delete_selected_keys">删除已选密钥</string>
+    <string name="error_publish_avatar_offline">需要连接到网络才能发布头像。</string>
     <string name="show_error_message">显示出错消息</string>
-    <string name="error_message">出错信息</string>
+    <string name="error_message">出错消息</string>
     <string name="data_saver_enabled">省流量模式已启用</string>
-    <string name="data_saver_enabled_explained">你的操作系统正限制 %1$s 在后台时访问互联网。要接收新消息通知,你应当在 \'数据节省\' 处于启用状态时允许 %1$s 不受限制的访问。\n%1$s 在可能的时候仍会试图节省数据。</string>
-    <string name="device_does_not_support_data_saver">你的设备不支持对 %1$s 禁用数据节省器。</string>
+    <string name="data_saver_enabled_explained">操作系统正限制 %1$s 在后台时访问互联网。要接收新消息通知,应当在“省流量模式”开启时允许 %1$s 无限制访问。
+\n%1$s 仍将在可能的情况下努力节省数据。</string>
+    <string name="device_does_not_support_data_saver">设备不支持对 %1$s 禁用省流量模式。</string>
     <string name="error_unable_to_create_temporary_file">无法创建临时文件</string>
-    <string name="this_device_has_been_verified">已验证此设备</string>
+    <string name="this_device_has_been_verified">此设备已经过验证</string>
     <string name="copy_fingerprint">复制指纹</string>
-    <string name="all_omemo_keys_have_been_verified">你已验证了你拥有的所有 OMEMO 密钥</string>
-    <string name="barcode_does_not_contain_fingerprints_for_this_conversation">条码不包含用于聊天的指纹。</string>
+    <string name="all_omemo_keys_have_been_verified">您已验证您拥有的所有 OMEMO 密钥</string>
+    <string name="barcode_does_not_contain_fingerprints_for_this_conversation">二维码中不包含此对话的指纹。</string>
     <string name="verified_fingerprints">已验证的指纹</string>
-    <string name="use_camera_icon_to_scan_barcode">使用相机扫描联系人条码</string>
-    <string name="please_wait_for_keys_to_be_fetched">请等待获取密钥</string>
-    <string name="share_as_barcode">分享条码</string>
-    <string name="share_as_uri">分享XMPP URI</string>
-    <string name="share_as_http">分享HTTP链接</string>
+    <string name="use_camera_icon_to_scan_barcode">使用相机扫描对方二维码</string>
+    <string name="please_wait_for_keys_to_be_fetched">正在获取密钥,请稍候</string>
+    <string name="share_as_barcode">分享为二维码</string>
+    <string name="share_as_uri">分享为 XMPP URI</string>
+    <string name="share_as_http">分享为 HTTP 链接</string>
     <string name="pref_blind_trust_before_verification">验证前盲目信任</string>
-    <string name="pref_blind_trust_before_verification_summary">自动信任陌生人的设备,但在验证过联系人添加设备时手动确认。</string>
-    <string name="blindly_trusted_omemo_keys">盲目信任的 OMEMO 密钥,表示它们可能时其他人或者某人可能冒充别人发送消息。</string>
+    <string name="pref_blind_trust_before_verification_summary">信任来自未经验证的联系人的新设备,但对于已验证的联系人,提示手动确认新设备。</string>
+    <string name="blindly_trusted_omemo_keys">盲目信任的 OMEMO 密钥,表示它们可能是其他人或者某人可能冒充别人发送消息。</string>
     <string name="not_trusted">不信任的</string>
-    <string name="invalid_barcode">无效二维码</string>
-    <string name="pref_clean_cache_summary">清理缓存文件夹(由相机应用使用)</string>
-    <string name="pref_clean_cache">清除缓存</string>
-    <string name="pref_clean_private_storage">清除私密存储</string>
-    <string name="pref_clean_private_storage_summary">清除保存私密文件的存储 (可以从服务器上重新下载)</string>
-    <string name="i_followed_this_link_from_a_trusted_source">此链接的源头是可信的</string>
-    <string name="verifying_omemo_keys_trusted_source">点击链接后将会开始校验%1$s的OMEMO密钥。只有%2$s发布的链接才是安全的。</string>
-    <string name="verifying_omemo_keys_trusted_source_account">您将验证您自己账户的 OMEMO 密钥。只有当您从可信的来源跟踪此链接时,这才是安全的。“可信”指的是此链接只可能是你在来源中发布的。</string>
+    <string name="invalid_barcode">二维码无效</string>
+    <string name="pref_clean_cache_summary">清理缓存文件夹(由相机应用使用)</string>
+    <string name="pref_clean_cache">清理缓存</string>
+    <string name="pref_clean_private_storage">清理私人存储空间</string>
+    <string name="pref_clean_private_storage_summary">清理保存文件的私人存储(它们可以从服务器重新下载)</string>
+    <string name="i_followed_this_link_from_a_trusted_source">我从可信来源收到此链接</string>
+    <string name="verifying_omemo_keys_trusted_source">点击链接后,您将验证 %1$s 的 OMEMO 密钥。只有从可信来源(只有 %2$s 可以发布此链接)收到此链接才是安全的。</string>
+    <string name="verifying_omemo_keys_trusted_source_account">您将验证自己账号的 OMEMO 密钥。只有从可信来源(只有您可以发布此链接)收到此链接才是安全的。</string>
     <string name="continue_btn">继续</string>
-    <string name="verify_omemo_keys">校验OMEMO密钥</string>
-    <string name="show_inactive_devices">显示不活跃设备</string>
-    <string name="hide_inactive_devices">隐藏不活跃设备</string>
+    <string name="verify_omemo_keys">验证 OMEMO 密钥</string>
+    <string name="show_inactive_devices">显示非活动设备</string>
+    <string name="hide_inactive_devices">隐藏非活动设备</string>
     <string name="distrust_omemo_key">不再信任设备</string>
-    <string name="distrust_omemo_key_text">你确认要移除此设备的验证吗?\n此设备及从其发送的信息将会被标识为不可信。</string>
+    <string name="distrust_omemo_key_text">是否确定要移除此设备的验证?
+\n此设备及其消息将标记为“不信任的”。</string>
     <plurals name="seconds">
-        <item quantity="other">%d秒</item>
+        <item quantity="other">%d 秒</item>
     </plurals>
     <plurals name="minutes">
-        <item quantity="other">%d分钟</item>
+        <item quantity="other">%d 分钟</item>
     </plurals>
     <plurals name="hours">
-        <item quantity="other">%d小时</item>
+        <item quantity="other">%d 小时</item>
     </plurals>
     <plurals name="days">
-        <item quantity="other">%d天</item>
+        <item quantity="other">%d 天</item>
     </plurals>
     <plurals name="weeks">
-        <item quantity="other">%d周</item>
+        <item quantity="other">%d 周</item>
     </plurals>
     <plurals name="months">
-        <item quantity="other">%d个月</item>
+        <item quantity="other">%d 个月</item>
     </plurals>
     <string name="pref_automatically_delete_messages">自动删除消息</string>
-    <string name="pref_automatically_delete_messages_description">自动从此设备上删除超过配置时间段的消息。</string>
-    <string name="encrypting_message">消息加密中</string>
-    <string name="not_fetching_history_retention_period">由于本地保留期限设置,无法提取消息。</string>
+    <string name="pref_automatically_delete_messages_description">从此设备上自动删除超过配置时限的消息。</string>
+    <string name="encrypting_message">正在加密消息</string>
+    <string name="not_fetching_history_retention_period">由于本地保留期限设置,无法获取消息。</string>
     <string name="transcoding_video">正在压缩视频</string>
     <string name="corresponding_conversations_closed">相应的对话已关闭。</string>
-    <string name="contact_blocked_past_tense">联系人已封禁。</string>
-    <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_entire_domain">封禁整个域名</string>
+    <string name="contact_blocked_past_tense">联系人已屏蔽。</string>
+    <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_entire_domain">屏蔽整个域名</string>
     <string name="online_right_now">当前在线</string>
     <string name="retry_decryption">重试解密</string>
     <string name="session_failure">会话失败</string>
-    <string name="sasl_downgrade">已降级的SASL机制</string>
+    <string name="sasl_downgrade">已降级的 SASL 机制</string>
     <string name="account_status_regis_web">服务器要求在网站上注册</string>
     <string name="open_website">打开网站</string>
-    <string name="application_found_to_open_website">没有可以打开网站的应用</string>
+    <string name="application_found_to_open_website">未找到可以打开网站的应用</string>
     <string name="pref_headsup_notifications">顶部通知</string>
     <string name="pref_headsup_notifications_summary">显示顶部通知</string>
     <string name="today">今天</string>
     <string name="yesterday">昨天</string>
-    <string name="pref_validate_hostname">通过DNSSEC验证主机名</string>
-    <string name="pref_validate_hostname_summary">包含主机名的服务器证书被认为是已验证的</string>
-    <string name="certificate_does_not_contain_jid">证书不包含XMPP地址</string>
-    <string name="server_info_partial">一部分</string>
+    <string name="pref_validate_hostname">用 DNSSEC 验证主机名</string>
+    <string name="pref_validate_hostname_summary">将包含主机名的服务器证书视为是已验证的</string>
+    <string name="certificate_does_not_contain_jid">证书不包含 XMPP 地址</string>
+    <string name="server_info_partial">部分</string>
     <string name="attach_record_video">录制视频</string>
-    <string name="copy_to_clipboard">复制</string>
-    <string name="message_copied_to_clipboard">消息已被复制</string>
+    <string name="copy_to_clipboard">复制到剪贴板</string>
+    <string name="message_copied_to_clipboard">消息已复制到剪贴板</string>
     <string name="message">消息</string>
-    <string name="private_messages_are_disabled">禁止私信</string>
+    <string name="private_messages_are_disabled">已禁用私信</string>
     <string name="huawei_protected_apps">受保护的应用</string>
-    <string name="huawei_protected_apps_summary">为了在屏幕关闭时也可收到消息提醒,您需要将Conversations加入受保护的应用列表。</string>
-    <string name="mtm_accept_cert">接受未知的证书?</string>
-    <string name="mtm_trust_anchor">服务器证书未由已知证书机构签发。</string>
+    <string name="huawei_protected_apps_summary">为了在屏幕关闭时也能收到消息提醒,您需要将 Conversations 加入受保护的应用列表。</string>
+    <string name="mtm_accept_cert">接受未知证书?</string>
+    <string name="mtm_trust_anchor">此服务器证书不是由已知的证书颁发机构签发的。</string>
     <string name="mtm_accept_servername">接受不匹配的服务器名称?</string>
-    <string name="mtm_hostname_mismatch">由于“%s”,服务器无法验证。证书仅对此有效:</string>
-    <string name="mtm_connect_anyway">您仍希望连接吗?</string>
-    <string name="mtm_cert_details">证书详情:</string>
+    <string name="mtm_hostname_mismatch">由于\"%s\",服务器无法验证。证书仅对此有效:</string>
+    <string name="mtm_connect_anyway">是否仍要连接?</string>
+    <string name="mtm_cert_details">证书详情:</string>
     <string name="once">仅一次</string>
-    <string name="qr_code_scanner_needs_access_to_camera">二维码扫描器需要摄像头权限</string>
-    <string name="pref_scroll_to_bottom">滚动到底部</string>
-    <string name="pref_scroll_to_bottom_summary">发送消息后滚动到底部</string>
+    <string name="qr_code_scanner_needs_access_to_camera">需要访问相机来扫描二维码</string>
+    <string name="pref_scroll_to_bottom">滚动至底部</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>
-    <string name="error_trustkey_general">%1$s 无法发送加密消息到 %2$s。这可能是因为你的联系人使用了过期的服务器或者无法处理 OMEMO 的客户端。</string>
+    <string name="error_trustkey_general">%1$s 无法发送加密消息到 %2$s。这可能是由于对方使用了过时的服务器或者无法处理 OMEMO 的客户端。</string>
     <string name="error_trustkey_device_list">无法获取设备列表</string>
     <string name="error_trustkey_bundle">无法获取密钥</string>
-    <string name="error_trustkey_hint_mutual">提示:某些情况下,可以将对方加入联系人列表,以解决此问题。</string>
-    <string name="disable_encryption_message">确认要禁用此会话的OMEMO加密吗?\n这会允许您的服务器管理员阅读你们的消息,但这可能是和使用过时客户端的人会话的唯一方式。</string>
+    <string name="error_trustkey_hint_mutual">提示:某些情况下,双方可以添加到联系人列表解决此问题。</string>
+    <string name="disable_encryption_message">是否确定要禁用此对话的 OMEMO 加密?
+\n这将允许服务器管理员读取您的消息,但这可能是与使用过时客户端的用户交流的唯一方法。</string>
     <string name="disable_now">立即禁用</string>
     <string name="draft">草稿:</string>
-    <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">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="create_shortcut">创建快捷方式</string>
     <string name="pref_font_size">字体大小</string>
     <string name="pref_font_size_summary">应用内使用的相对字体大小。</string>

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

@@ -12,6 +12,7 @@
 
     <attr name="IconSize" format="dimension" />
 
+    <attr name="colorPrimary" format="reference|color" />
     <attr name="color_background_tertiary" format="reference|color" />
     <attr name="color_background_secondary" format="reference|color" />
     <attr name="color_background_primary" format="reference|color" />
@@ -75,6 +76,7 @@
     <attr name="media_preview_tour" format="reference" />
     <attr name="media_preview_contact" format="reference" />
     <attr name="media_preview_app" format="reference" />
+    <attr name="media_preview_audiobook" format="reference" />
     <attr name="media_preview_calendar" format="reference" />
     <attr name="media_preview_archive" format="reference" />
     <attr name="media_preview_ebook" format="reference" />
@@ -86,6 +88,7 @@
     <attr name="icon_add_person" format="reference" />
     <attr name="icon_cancel" format="reference" />
     <attr name="icon_copy" format="reference" />
+    <attr name="icon_qr_code" format="reference" />
     <attr name="icon_discard" format="reference" />
     <attr name="icon_download" format="reference" />
     <attr name="icon_edit" format="reference" />

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

@@ -154,6 +154,7 @@
     <string name="error_security_exception">The app you used to share this file did not provide enough permissions.</string>
     <string name="account_status_unknown">Unknown</string>
     <string name="account_status_disabled">Temporarily disabled</string>
+    <string name="account_state_logged_out">Logged out</string>
     <string name="account_status_online">Online</string>
     <string name="account_status_connecting">Connecting\u2026</string>
     <string name="account_status_offline">Offline</string>
@@ -421,6 +422,7 @@
     <string name="multimedia_file">multimedia file</string>
     <string name="pdf_document">PDF document</string>
     <string name="apk">Android App</string>
+    <string name="audiobook">Audiobook</string>
     <string name="vcard">Contact</string>
     <string name="avatar_has_been_published">Avatar has been published!</string>
     <string name="sending_x_file">Sending %s</string>
@@ -539,6 +541,7 @@
     <string name="send_corrected_message">Send corrected message</string>
     <string name="no_keys_just_confirm">You have already validated this persons fingerprint securely to confirm trust. By selecting “Done” you are just confirming that %s is part of this group chat.</string>
     <string name="this_account_is_disabled">You have disabled this account</string>
+    <string name="this_account_is_logged_out">You have logged out of this account</string>
     <string name="security_error_invalid_file_access">Security error: Invalid file access!</string>
     <string name="no_application_to_share_uri">No app found to share URI</string>
     <string name="share_uri_with">Share URI with…</string>
@@ -589,6 +592,7 @@
     <string name="type_web">Web browser</string>
     <string name="type_console">Console</string>
     <string name="payment_required">Payment required</string>
+    <string name="reconnect_on_other_host">Reconnect on other host</string>
     <string name="missing_internet_permission">Grant permission to use the Internet</string>
     <string name="me">Me</string>
     <string name="contact_asks_for_presence_subscription">Contact asks for presence subscription</string>
@@ -898,6 +902,7 @@
     <string name="event">Event</string>
     <string name="open_backup">Open backup</string>
     <string name="not_a_backup_file">The file you selected is not a Conversations backup file</string>
+    <string name="outdated_backup_file_format">You are trying to import an outdated backup file format</string>
     <string name="account_already_setup">This account has already been setup</string>
     <string name="please_enter_password">Please enter the password for this account</string>
     <string name="unable_to_perform_this_action">Could not perform this action</string>
@@ -1018,5 +1023,9 @@
     <string name="decline">Decline</string>
     <string name="delete_from_server">Remove account from server</string>
     <string name="could_not_delete_account_from_server">Could not delete account from server</string>
-
+    <string name="hide_notification">Hide notification</string>
+    <string name="log_out">Log out</string>
+    <string name="log_in">Log in</string>
+    <string name="contact_uses_unverified_keys">Your contact uses unverified devices. Scan their 2D barcode to perform verification and impede active MITM attacks.</string>
+    <string name="unverified_devices">You are using unverified devices. Scan the 2D barcodes of your other devices to perform verification and impede active MITM attack.</string>
 </resources>

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

@@ -88,6 +88,7 @@
         <item name="media_preview_tour" type="reference">@drawable/baseline_tour_black_48</item>
         <item name="media_preview_contact" type="reference">@drawable/ic_person_black_48dp</item>
         <item name="media_preview_app" type="reference">@drawable/ic_android_black_48dp</item>
+        <item name="media_preview_audiobook" type="reference">@drawable/ic_play_lesson_black_24</item>
         <item name="media_preview_calendar" type="reference">@drawable/ic_event_black_48dp</item>
         <item name="media_preview_archive" type="reference">@drawable/ic_archive_black_48dp</item>
         <item name="media_preview_ebook" type="reference">@drawable/ic_book_black_48dp</item>
@@ -97,7 +98,7 @@
         <item name="icon_add_group" type="reference">@drawable/ic_group_add_white_24dp</item>
         <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item>
         <item name="icon_cancel" type="reference">@drawable/ic_cancel_black_24dp</item>
-        <item name="icon_copy" type="reference">@drawable/ic_content_copy_black_24dp</item>
+        <item name="icon_qr_code" type="reference">@drawable/ic_qr_code_black_24dp</item>
         <item name="icon_discard" type="reference">@drawable/ic_delete_white_24dp</item>
         <item name="icon_download" type="reference">@drawable/ic_file_download_white_24dp</item>
         <item name="icon_edit" type="reference">@drawable/ic_edit_white_24dp</item>
@@ -244,6 +245,7 @@
         <item name="media_preview_tour" type="reference">@drawable/baseline_tour_white_48</item>
         <item name="media_preview_contact" type="reference">@drawable/ic_person_white_48dp</item>
         <item name="media_preview_app" type="reference">@drawable/ic_android_white_48dp</item>
+        <item name="media_preview_audiobook" type="reference">@drawable/ic_play_lesson_white_48dp</item>
         <item name="media_preview_calendar" type="reference">@drawable/ic_event_white_48dp</item>
         <item name="media_preview_archive" type="reference">@drawable/ic_archive_white_48dp</item>
         <item name="media_preview_ebook" type="reference">@drawable/ic_book_white_48dp</item>
@@ -253,7 +255,7 @@
         <item name="icon_add_group" type="reference">@drawable/ic_group_add_white_24dp</item>
         <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item>
         <item name="icon_cancel" type="reference">@drawable/ic_cancel_white_24dp</item>
-        <item name="icon_copy" type="reference">@drawable/ic_content_copy_white_24dp</item>
+        <item name="icon_qr_code" type="reference">@drawable/ic_qr_code_white_24dp</item>
         <item name="icon_discard" type="reference">@drawable/ic_delete_white_24dp</item>
         <item name="icon_download" type="reference">@drawable/ic_file_download_white_24dp</item>
         <item name="icon_edit" type="reference">@drawable/ic_edit_white_24dp</item>

src/main/res/xml/data_extraction_rules.xml 🔗

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-extraction-rules>
+    <cloud-backup disableIfNoEncryptionCapabilities="true">
+        <include domain="sharedpref" />
+        <include domain="database" />
+    </cloud-backup>
+    <device-transfer>
+        <include domain="sharedpref" />
+        <include domain="database" />
+    </device-transfer>
+</data-extraction-rules>

src/main/res/xml/locales_config.xml 🔗

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
+    <locale android:name="en-US" />
+    <locale android:name="ar" />
+    <locale android:name="bg" />
+    <locale android:name="bn-IN" />
+    <locale android:name="ca" />
+    <locale android:name="cs" />
+    <locale android:name="da-DK" />
+    <locale android:name="de" />
+    <locale android:name="el" />
+    <locale android:name="es" />
+    <locale android:name="eu" />
+    <locale android:name="fa-IR" />
+    <locale android:name="fi" />
+    <locale android:name="fr" />
+    <locale android:name="gl" />
+    <locale android:name="hi-IN" />
+    <locale android:name="hr" />
+    <locale android:name="hu" />
+    <locale android:name="id" />
+    <locale android:name="it" />
+    <locale android:name="iw" />
+    <locale android:name="ja" />
+    <locale android:name="ko" />
+    <locale android:name="ml" />
+    <locale android:name="nb-NO" />
+    <locale android:name="nl" />
+    <locale android:name="pl" />
+    <locale android:name="pt" />
+    <locale android:name="pt-BR" />
+    <locale android:name="ro-RO" />
+    <locale android:name="ru" />
+    <locale android:name="sk" />
+    <locale android:name="sq-AL" />
+    <locale android:name="sr" />
+    <locale android:name="sv" />
+    <locale android:name="szl" />
+    <locale android:name="tr-TR" />
+    <locale android:name="uk" />
+    <locale android:name="vi" />
+    <locale android:name="zh-CN" />
+    <locale android:name="zh-TW" />
+</locale-config>

src/quicksy/fastlane/metadata/android/de-DE/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy ist ein Ableger des beliebten Jabber/XMPP-Clients Conversations mit automatischer Kontaktsuche.
+
+Du meldest dich mit deiner Telefonnummer an und Quicksy schlägt dir automatisch — basierend auf den Telefonnummern in deinem Adressbuch — mögliche Kontakte vor.
+
+Unter der Oberfläche ist Quicksy ein vollwertiger Jabber-Client, mit dem du mit jedem Benutzer auf jedem öffentlich zugänglichen Server kommunizieren kannst. Ebenso können Benutzer auf Quicksy von außen kontaktiert werden, indem du einfach +telefonnummer@quicksy.im zu deiner Kontaktliste hinzufügst.
+
+Abgesehen von der Kontaktsynchronisation ist die Benutzeroberfläche bewusst so nah wie möglich an Conversations gehalten. Dies ermöglicht es den Nutzern, von Quicksy zu Conversations zu wechseln, ohne die Funktionsweise der App neu erlernen zu müssen.
+
+Die vorgeschlagenen Kontakte bestehen aus anderen Quicksy-Benutzern und normalen Jabber/XMPP-Benutzern, die ihre Jabber-ID in das Quicksy-Verzeichnis (https://quicksy.im/#get-listed) eingegeben haben.
+
+HINWEIS: Für den Eintrag (https://quicksy.im/enter/) deiner Jabber-ID in das Quicksy-
+Verzeichnis einzutragen, ist eine einmalige Registrierungsgebühr erforderlich.
+
+Lies die Datenschutzrichtlinien (https://quicksy.im/#privacy) für weitere Informationen.

src/quicksy/fastlane/metadata/android/en-US/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy is a spin off of the popular Jabber/XMPP client Conversations with automatic contact discovery.
+
+You sign up with your phone number and Quicksy will automatically—based on the phone numbers in your address book—suggest possible contacts to you.
+
+Under the hood Quicksy is a full-fledged Jabber client that lets you communicate with any user on any publicly federating server. Likewise users on Quicksy can be contacted from the outside simply by adding +phonenumber@quicksy.im to your contact list.
+
+Aside from the contact sync the user interface is deliberately as close to Conversations as possible. This allows users to eventually migrate from Quicksy to Conversations without having to relearn how the app works.
+
+Suggested contacts consists of other Quicksy users and regular Jabber/XMPP users who have entered their Jabber ID into the Quicksy Directory (https://quicksy.im/#get-listed).
+
+NOTE: To enter (https://quicksy.im/enter/) your Jabber ID in the Quicksy
+Directory an one time registration fee is required.
+
+Read the Privacy Policy (https://quicksy.im/#privacy) for more info.

src/quicksy/fastlane/metadata/android/gl-ES/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy é unha aplicación derivada do coñecido cliente Conversations para Jabber/XMPP co engadido do descubremento automático dos contactos.
+
+Accedes co teu número de móbil e Quicksy suxerirá automáticamente posibles contactos en función dos números da libreta de enderezos.
+
+Por debaixo estarás desfrutando dun completo cliente Jabber que che permite comunicarte con calquera usuaria doutros servidores federados. Do mesmo xeito, as persoas que usan Quicksy poden ser contactadas simplemente engadindo +numerodemobil@quicksy.im á túa lista de contactos.
+
+Fóra da sincronización de contactos o resto da interface é o máis semellante posible a Conversations. Deste xeito as usuarias poden migrar de Quicksy a Conversations sen maiores dificultades e sen ter que volver a aprender a usar a aplicación.
+
+Os contactos suxeridos proveñen doutras usuarias de Quicksy e usuarias regulares de Jabber/XMPP que engadiron o seu ID de Jabber ao Directorio Quicksy (https://quicksy.im/#get-listed).
+
+NOTA: Para engadir (https://quicksy.im/enter/) o teu ID de Jabber ao Directorio
+Quicksy requírese facer unha pequena aportación só unha vez.
+
+Le a Política de Privacidade (https://quicksy.im/#privacy) para ter máis información.

src/quicksy/fastlane/metadata/android/it-IT/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy è uno spin off del popolare client Jabber/XMPP Conversations con ricerca automatica dei contatti.
+
+Ti registri con il numero di telefono e Quicksy ti proporrà automaticamente, in base ai numeri di telefono nella tua rubrica, i possibili contatti.
+
+Sotto il cofano Quicksy è un vero e proprio client Jabber che ti consente di comunicare con qualsiasi utente su qualsiasi server federato pubblicamente. Allo stesso modo gli utenti su Quicksy possono essere contattati dall'esterno semplicemente aggiungendo +numeroditelefono@quicksy.im al tuo elenco di contatti.
+
+A parte la sincronizzazione dei contatti, l'interfaccia utente è deliberatamente il più possibile simile a quella di Conversations. Ciò permette agli utenti eventualmente di migrare da Quicksy a Conversations senza il bisogno di imparare di nuovo come funziona l'app.
+
+I contatti proposti consistono in altri utenti di Quicksy e utenti regolari di Jabber/XMPP che hanno inserito il loro ID Jabber nella Directory di Quicksy (https://quicksy.im/#get-listed).
+
+NOTA: per inserire (https://quicksy.im/enter/) il tuo ID Jabber nella Directory
+di Quicksy è richiesto un pagamento una tantum per la registrazione.
+
+Leggi l'informativa sulla privacy (https://quicksy.im/#privacy) per maggiori informazioni.

src/quicksy/fastlane/metadata/android/ro/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy este un derivat al popularului client Jabber/XMPP Conversations cu descoperire automată a contactelor.
+
+Vă înscrieți cu numărul de telefon, iar Quicksy vă va sugera automat, pe baza numerelor de telefon din agenda dvs., posibile contacte.
+
+Sub capota Quicksy este un client Jabber complet care vă permite să comunicați cu orice utilizator de pe orice server public federat. De asemenea, utilizatorii de pe Quicksy pot fi contactați din exterior prin simpla adăugare a +numărdetelefon@quicksy.im la lista dvs. de contacte.
+
+În afară de sincronizarea contactelor, interfața utilizatorului este în mod deliberat cât mai apropiată de Conversations. Acest lucru permite utilizatorilor să migreze în cele din urmă de la Quicksy la Conversations fără a fi nevoiți să învețe din nou cum funcționează aplicația.
+
+Contactele sugerate constau din alți utilizatori Quicksy și utilizatori obișnuiți de Jabber/XMPP care și-au introdus adresa XMPP în Directorul Quicksy (https://quicksy.im/#get-listed).
+
+NOTĂ: Pentru a vă introduce (https://quicksy.im/enter/) adresa XMPP în Directorul
+Quicksy este necesară o taxă de înregistrare unică.
+
+Citiți Politica de confidențialitate (https://quicksy.im/#privacy) pentru mai multe informații.

src/quicksy/fastlane/metadata/android/uk/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy є відгалуженням Conversations — популярного клієнта Jabber/XMPP, з автоматичним пошуком контактів.
+
+Реєструйтеся за допомогою свого номера телефону, і Quicksy автоматично — за номерами телефонів у Вашій адресній книзі — запропонує Вам можливі контакти.
+
+Під капотом Quicksy — це повноцінний клієнт Jabber, який дозволяє вам спілкуватися з будь-яким користувачем на будь-якому загальнодоступному сервері. Так само з користувачами Quicksy можна зв’язатися ззовні, просто додавши +phonenumber@quicksy.im до свого списку контактів.
+
+Окрім синхронізації контактів, інтерфейс користувача навмисно максимально наближений до Conversations. Це дозволяє користувачам зрештою перейти з Quicksy на Conversations без необхідності перевчатися.
+
+Пропоновані контакти складаються з інших користувачів Quicksy і звичайних користувачів Jabber/XMPP, які ввели свій Jabber ID у каталог Quicksy (https://quicksy.im/#get-listed).
+
+ПРИМІТКА. Щоб ввести (https://quicksy.im/enter/) свій Jabber ID у каталог Quicksy,
+потрібно сплатити одноразовий реєстраційний внесок.
+
+Для отримання додаткової інформації прочитайте Політику конфіденційності (https://quicksy.im/#privacy).

src/quicksy/fastlane/metadata/android/zh-CN/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy 是一个流行的 Jabber/XMPP 客户端 Conversations 的衍生品,具有自动发现联系人的功能。
+
+您只需用电话号码注册,Quicksy 就会自动—根据您通讯录中的电话号码—向您推荐可能的联系人。
+
+从本质上讲,Quicksy 是一个成熟的 Jabber 客户端,可让您与任何公共联合服务器上的任何用户进行交流。同样,只需将 +phonenumber@quicksy.im 添加到您的联系人列表中,即可从外部联系 Quicksy 上的用户。
+
+除了联系人同步之外,用户界面尽可能地接近 Conversations。这使得用户最终可以从 Quicksy 迁移到 Conversations,而无需重新了解应用程序的工作方式。
+
+建议的联系人包括其他 Quicksy 用户和在 Quicksy 目录中输入 Jabber ID 的普通 Jabber/XMPP 用户 (https://quicksy.im/#get-listed)。
+
+注意:要在 Quicksy 中输入 (https://quicksy.im/enter/) 您的 Jabber ID
+目录一次性注册费用是必需的。
+
+请阅读隐私政策 (https://quicksy.im/#privacy) ,了解更多信息。

src/quicksy/res/drawable/ic_launcher_foreground.xml 🔗

@@ -1,19 +1,13 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="108dp"
-        android:height="108dp"
-        android:viewportWidth="49.41353"
-        android:viewportHeight="49.413532">
-    <group android:translateX="13.888052"
-            android:translateY="13.888054">
-      <path

src/quicksy/res/drawable/ic_launcher_monochrome.xml 🔗

@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="49.41353"
+    android:viewportHeight="49.413532">
+    <group
+        android:translateX="13.888052"
+        android:translateY="13.888054">
+        <path
+            android:fillColor="#000000"

src/quicksy/res/mipmap-anydpi-v26/new_launcher.xml 🔗

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@mipmap/ic_launcher_background"/>
-    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
-</adaptive-icon>

src/quicksy/res/mipmap-anydpi-v26/new_launcher_round.xml 🔗

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@mipmap/ic_launcher_background"/>
-    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
-</adaptive-icon>

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

@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="pref_notification_grace_period_summary">Время, на которое уведомления от Quicksy будут отключены, когда вы пользуетесь аккаунтом на другом устройстве.</string>
+    <string name="pref_notification_grace_period_summary">Время, на которое уведомления от Quicksy будут отключены после активности с другого устройства</string>
     <string name="pref_never_send_crash_summary">Отправляя отчёты об ошибках, вы помогаете в разработке Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">Извещать собеседников, когда вы пользуетесь Quicksy</string>
     <string name="huawei_protected_apps_summary">Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Quicksy в список защищенных приложений.</string>
-    <string name="set_profile_picture">Аватар для Quicksy</string>
-    <string name="not_available_in_your_country">Quicksy не доступно в вашем регионе</string>
+    <string name="set_profile_picture">Аватар профиля Quicksy</string>
+    <string name="not_available_in_your_country">Quicksy недоступен в Вашем регионе.</string>
     <string name="unable_to_verify_server_identity">Не удалось подтвердить сервер.</string>
     <string name="unknown_security_error">Неизвестная ошибка безопасности.</string>
     <string name="timeout_while_connecting_to_server">Время ожидания подключения к серверу вышло.</string>
-</resources>
+</resources>

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

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="pref_notification_grace_period_summary">Başka bir aygıt üstünde etkinlik algılandığında Quicksy\'nin sessiz kalma süresi</string>
-    <string name="pref_never_send_crash_summary">Çöküş raporu göndermeniz Quicksy\'nin geliştirlmesinde katkıda bulunacaktır.</string>
+    <string name="pref_never_send_crash_summary">Çöküş raporu göndermeniz Quicksy\'nin geliştirlmesinde katkıda bulunacaktır</string>
     <string name="pref_broadcast_last_activity_summary">Tüm kişileriniz ne zaman Quicksy kullandığınızı görsün</string>
     <string name="huawei_protected_apps_summary">Ekranınız kapalıyken bile bildirim almak için Quicksy\'i korunan uygulamalara eklemelisiniz.</string>
     <string name="set_profile_picture">Quicksy profil resmi</string>
@@ -9,4 +9,4 @@
     <string name="unable_to_verify_server_identity">Sunucu kimliği belirlenemiyor.</string>
     <string name="unknown_security_error">Bilinmeyen güvenlik hatası.</string>
     <string name="timeout_while_connecting_to_server">Sunucuya bağlanılırken zaman aşımına uğrandı.</string>
-</resources>
+</resources>

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

@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="pref_notification_grace_period_summary">Час, протягом якого застосунок дотримується тиші після активності на іншому пристрої.</string>
-    <string name="pref_never_send_crash_summary">Надсилаючи траси стеків виклику, ви допомагаєте розробці цього застосунку.</string>
-    <string name="pref_broadcast_last_activity_summary">Дозволити всім вашим контактам знати, коли ви використовуєте цю програму.</string>
-    <string name="huawei_protected_apps_summary">Щоб продовжувати отримувати сповіщення, навіть коли екран вимкнуто, потрібно додати цю програму до списку захищених.</string>
+    <string name="pref_notification_grace_period_summary">Час, протягом якого застосунок дотримується тиші після виявлення активності на іншому пристрої</string>
+    <string name="pref_never_send_crash_summary">Надсилаючи траси стеку викликів, Ви допомагаєте розробляти Quicksy</string>
+    <string name="pref_broadcast_last_activity_summary">Повідомляти співрозмовникам, що Ви користуєтеся Quicksy</string>
+    <string name="huawei_protected_apps_summary">Щоб отримувати сповіщення навіть коли екран погас, необхідно додати Quicksy до списку захищених програм.</string>
     <string name="set_profile_picture">Зображення профілю для Quicksy</string>
-    <string name="not_available_in_your_country">Цей застосунок не доступний у вашій країні.</string>
+    <string name="not_available_in_your_country">Цей застосунок недоступний у Вашій країні.</string>
     <string name="unable_to_verify_server_identity">Автентичність сервера не підтверджено.</string>
     <string name="unknown_security_error">Невідома помилка безпеки.</string>
     <string name="timeout_while_connecting_to_server">Вичерпано час для встановлення з\'єднання із сервером.</string>
-</resources>
+</resources>

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

@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="pref_notification_grace_period_summary">发现在其它设备上的活动后,Conversations保持安静的时间</string>
-    <string name="pref_never_send_crash_summary">通过发送堆栈跟踪,您可以帮助 Quicksy 的持续开发</string>
-    <string name="pref_broadcast_last_activity_summary">让你的所有联系人知道你使用Quicksy的时间</string>
+    <string name="pref_notification_grace_period_summary">发现另一台设备上的活动后,Quicksy 在此期间保持安静</string>
+    <string name="pref_never_send_crash_summary">通过发送堆栈跟踪,您正在帮助 Quicksy 的持续开发</string>
+    <string name="pref_broadcast_last_activity_summary">让您的所有联系人知道您最后使用 Quicksy 的时间</string>
     <string name="huawei_protected_apps_summary">为了在屏幕关闭时也能收到消息提醒,您需要将 Quicksy 加入受保护的应用列表。</string>
     <string name="set_profile_picture">Quicksy 个人资料图片</string>
-    <string name="not_available_in_your_country">Quicksy在您的国家无服务。</string>
+    <string name="not_available_in_your_country">Quicksy 在您所在的国家/地区无法使用。</string>
     <string name="unable_to_verify_server_identity">无法验证服务器身份。</string>
-    <string name="unknown_security_error">未知安全错误。</string>
+    <string name="unknown_security_error">安全错误未知。</string>
     <string name="timeout_while_connecting_to_server">连接到服务器时超时。</string>
 </resources>