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

Stephen Paul Weber created

* 'master' of https://codeberg.org/iNPUTmice/Conversations: (363 commits)
  version bump to 2.15.3 + changelog
  make restore backup dialog scrollable
  introduce setting to ignore invites from strangers
  add realme x50 to bad devices list
  update gradle wrapper
  fix formating for previous commit
  getting and setting audio devices on Android 14 when not integrated should go to fallback
  stop linter from warning about feature check
  version bump to 2.15.2
  delete 'screenshots' compilation image
  link to individual fastlane screenshots instead of compilation image
  delay integrated audio routing on callee end until picked up
  Update fastlane screenshots Co-authored-by: Licaon_Kter <licaon-kter@noreply.codeberg.org> Co-committed-by: Licaon_Kter <licaon-kter@noreply.codeberg.org>
  Translated using Weblate (Polish)
  Translated using Weblate (Polish)
  Translated using Weblate (Polish)
  Translated using Weblate (Galician)
  Translated using Weblate (Polish)
  Translated using Weblate (Galician)
  Translated using Weblate (Polish)
  ...

Change summary

CHANGELOG.md                                                                                 |  36 
build.gradle                                                                                 |  20 
fastlane/metadata/android/de-DE/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/de-DE/changelogs/4210404.txt                                       |   3 
fastlane/metadata/android/de-DE/changelogs/4210504.txt                                       |   2 
fastlane/metadata/android/de-DE/changelogs/4210604.txt                                       |   3 
fastlane/metadata/android/de-DE/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/de-DE/changelogs/4210804.txt                                       |   2 
fastlane/metadata/android/en-US/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/en-US/changelogs/4210404.txt                                       |   3 
fastlane/metadata/android/en-US/changelogs/4210504.txt                                       |   2 
fastlane/metadata/android/en-US/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/en-US/changelogs/4210804.txt                                       |   2 
fastlane/metadata/android/en-US/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/en-US/changelogs/4211004.txt                                       |   2 
fastlane/metadata/android/es-ES/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/es-ES/changelogs/4210404.txt                                       |   3 
fastlane/metadata/android/es-ES/changelogs/4210504.txt                                       |   2 
fastlane/metadata/android/es-ES/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/gl-ES/changelogs/4209404.txt                                       |   1 
fastlane/metadata/android/gl-ES/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/gl-ES/changelogs/4210404.txt                                       |   3 
fastlane/metadata/android/gl-ES/changelogs/4210504.txt                                       |   2 
fastlane/metadata/android/gl-ES/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/gl-ES/changelogs/4210804.txt                                       |   2 
fastlane/metadata/android/it-IT/changelogs/4209204.txt                                       |   2 
fastlane/metadata/android/it-IT/changelogs/4209404.txt                                       |   1 
fastlane/metadata/android/it-IT/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/it-IT/changelogs/4210404.txt                                       |   3 
fastlane/metadata/android/it-IT/changelogs/4210504.txt                                       |   2 
fastlane/metadata/android/it-IT/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/it-IT/changelogs/4210804.txt                                       |   2 
fastlane/metadata/android/pl-PL/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/pl-PL/changelogs/4210804.txt                                       |   2 
fastlane/metadata/android/ru-RU/changelogs/4209004.txt                                       |   2 
fastlane/metadata/android/ru-RU/changelogs/4209204.txt                                       |   2 
fastlane/metadata/android/ru-RU/changelogs/4209404.txt                                       |   1 
fastlane/metadata/android/ru-RU/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/sq/changelogs/42037.txt                                            |  10 
fastlane/metadata/android/sq/changelogs/42050.txt                                            |   1 
fastlane/metadata/android/sq/changelogs/42059.txt                                            |   2 
fastlane/metadata/android/sq/changelogs/42060.txt                                            |   1 
fastlane/metadata/android/sq/changelogs/42061.txt                                            |   1 
fastlane/metadata/android/sq/changelogs/42062.txt                                            |   1 
fastlane/metadata/android/sq/changelogs/42065.txt                                            |   1 
fastlane/metadata/android/sq/changelogs/42068.txt                                            |   2 
fastlane/metadata/android/sq/changelogs/42072.txt                                            |   3 
fastlane/metadata/android/sq/changelogs/4207704.txt                                          |   3 
fastlane/metadata/android/sq/changelogs/4208104.txt                                          |   4 
fastlane/metadata/android/sq/changelogs/4208804.txt                                          |   3 
fastlane/metadata/android/sq/changelogs/4209004.txt                                          |   2 
fastlane/metadata/android/sq/changelogs/4209204.txt                                          |   2 
fastlane/metadata/android/sq/changelogs/4209404.txt                                          |   1 
fastlane/metadata/android/sq/changelogs/4210104.txt                                          |   1 
fastlane/metadata/android/sq/changelogs/4210404.txt                                          |   3 
fastlane/metadata/android/sq/changelogs/4210504.txt                                          |   2 
fastlane/metadata/android/sq/changelogs/4210704.txt                                          |   3 
fastlane/metadata/android/uk/changelogs/4210104.txt                                          |   1 
fastlane/metadata/android/uk/changelogs/4210404.txt                                          |   3 
fastlane/metadata/android/uk/changelogs/4210504.txt                                          |   2 
fastlane/metadata/android/uk/changelogs/4210704.txt                                          |   3 
fastlane/metadata/android/uk/changelogs/4210804.txt                                          |   2 
fastlane/metadata/android/zh-CN/changelogs/42018.txt                                         |   2 
fastlane/metadata/android/zh-CN/changelogs/42041.txt                                         |   2 
fastlane/metadata/android/zh-CN/changelogs/4209404.txt                                       |   1 
fastlane/metadata/android/zh-CN/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/zh-CN/changelogs/4210404.txt                                       |   3 
fastlane/metadata/android/zh-CN/changelogs/4210504.txt                                       |   2 
fastlane/metadata/android/zh-CN/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/zh-CN/changelogs/4210804.txt                                       |   2 
gradle/wrapper/gradle-wrapper.properties                                                     |   6 
gradlew                                                                                      |  41 
gradlew.bat                                                                                  |  35 
proguard-rules.pro                                                                           |   1 
screenshots.png                                                                              |   0 
screenshots.xcf                                                                              |   0 
src/cheogram/java/com/cheogram/android/ConnectionService.java                                |  29 
src/cheogram/java/com/cheogram/android/DownloadDefaultStickers.java                          |   2 
src/cheogram/java/com/cheogram/android/TagEditorView.java                                    |   9 
src/cheogram/java/com/cheogram/android/WebxdcPage.java                                       |   3 
src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java                |   2 
src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java                        |  10 
src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java                         |  17 
src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java                       |  11 
src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java                          |   6 
src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java                     |  11 
src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java                             |   7 
src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java                   |   8 
src/cheogram/res/drawable/background_splash_screen.xml                                       |   2 
src/cheogram/res/drawable/business_black.xml                                                 |  10 
src/cheogram/res/drawable/email_black.xml                                                    |  10 
src/cheogram/res/drawable/email_white.xml                                                    |  10 
src/cheogram/res/drawable/ic_business_24dp.xml                                               |   2 
src/cheogram/res/drawable/ic_email_24dp.xml                                                  |  12 
src/cheogram/res/drawable/ic_link_24dp.xml                                                   |   2 
src/cheogram/res/drawable/ic_logo_mono.xml                                                   |  17 
src/cheogram/res/drawable/ic_mood_24dp.xml                                                   |  10 
src/cheogram/res/drawable/irc.xml                                                            |   3 
src/cheogram/res/drawable/link_black.xml                                                     |  10 
src/cheogram/res/drawable/list_choice.xml                                                    |   9 
src/cheogram/res/drawable/matrix.xml                                                         |   7 
src/cheogram/res/drawable/palette_24px.xml                                                   |   8 
src/cheogram/res/drawable/pill.xml                                                           |   2 
src/cheogram/res/drawable/subject.xml                                                        |   6 
src/cheogram/res/drawable/textsend.xml                                                       |   2 
src/cheogram/res/layout/actionview_edit.xml                                                  |   1 
src/cheogram/res/layout/activity_easy_invite.xml                                             |  39 
src/cheogram/res/layout/activity_import_backup.xml                                           |  27 
src/cheogram/res/layout/activity_magic_create.xml                                            |  54 
src/cheogram/res/layout/activity_pick_server.xml                                             |  38 
src/cheogram/res/layout/activity_welcome.xml                                                 |  52 
src/cheogram/res/layout/command_button.xml                                                   |   8 
src/cheogram/res/layout/command_button_grid_field.xml                                        |   9 
src/cheogram/res/layout/command_checkbox_field.xml                                           |   4 
src/cheogram/res/layout/command_note.xml                                                     |   8 
src/cheogram/res/layout/command_progress_bar.xml                                             |   6 
src/cheogram/res/layout/command_radio_edit_field.xml                                         |   8 
src/cheogram/res/layout/command_result_cell.xml                                              |   3 
src/cheogram/res/layout/command_result_field.xml                                             |   7 
src/cheogram/res/layout/command_search_list_field.xml                                        |   8 
src/cheogram/res/layout/command_slider_field.xml                                             |  11 
src/cheogram/res/layout/command_spinner_field.xml                                            |   7 
src/cheogram/res/layout/command_text_field.xml                                               |   5 
src/cheogram/res/layout/command_webview.xml                                                  |   3 
src/cheogram/res/layout/dialog_enter_password.xml                                            |  76 
src/cheogram/res/layout/emoji_search.xml                                                     |   1 
src/cheogram/res/layout/emoji_search_row.xml                                                 |  14 
src/cheogram/res/layout/thread_row.xml                                                       |   4 
src/cheogram/res/menu/conversations.xml                                                      |   4 
src/cheogram/res/menu/easy_onboarding_invite.xml                                             |   2 
src/cheogram/res/menu/manageaccounts.xml                                                     |  53 
src/cheogram/res/menu/welcome_menu.xml                                                       |   2 
src/cheogram/res/values/colors-themed.xml                                                    |  63 
src/cheogram/res/values/colors.xml                                                           |  16 
src/cheogram/res/values/styles.xml                                                           |  10 
src/cheogram/res/values/themes.xml                                                           | 585 
src/conversations/AndroidManifest.xml                                                        |   1 
src/conversations/fastlane/metadata/android/fr-FR/full_description.txt                       |  38 
src/conversations/fastlane/metadata/android/fr-FR/short_description.txt                      |   1 
src/conversations/fastlane/metadata/android/ja-JP/short_description.txt                      |   1 
src/conversations/fastlane/metadata/android/ru-RU/full_description.txt                       |  26 
src/conversations/fastlane/metadata/android/ru-RU/short_description.txt                      |   1 
src/conversations/fastlane/metadata/android/zh-CN/full_description.txt                       |   8 
src/conversations/fastlane/metadata/android/zh-CN/short_description.txt                      |   2 
src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java              |  12 
src/conversations/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java           |  70 
src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java                   | 258 
src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java                    | 141 
src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java                  | 113 
src/conversations/java/eu/siacs/conversations/ui/PickServerActivity.java                     |  17 
src/conversations/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java                |  11 
src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java                        |  94 
src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java              |  13 
src/conversations/res/drawable-hdpi/ic_notification.png                                      |   0 
src/conversations/res/drawable-hdpi/ic_unarchive_white_24dp.png                              |   0 
src/conversations/res/drawable-mdpi/ic_notification.png                                      |   0 
src/conversations/res/drawable-mdpi/ic_unarchive_white_24dp.png                              |   0 
src/conversations/res/drawable-xhdpi/ic_notification.png                                     |   0 
src/conversations/res/drawable-xhdpi/ic_unarchive_white_24dp.png                             |   0 
src/conversations/res/drawable-xxhdpi/ic_notification.png                                    |   0 
src/conversations/res/drawable-xxhdpi/ic_unarchive_white_24dp.png                            |   0 
src/conversations/res/drawable-xxxhdpi/ic_notification.png                                   |   0 
src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png                           |   0 
src/conversations/res/drawable/ic_app_icon_notification.xml                                  |  11 
src/conversations/res/layout/activity_easy_invite.xml                                        |  32 
src/conversations/res/layout/activity_import_backup.xml                                      |  27 
src/conversations/res/layout/activity_magic_create.xml                                       |  54 
src/conversations/res/layout/activity_pick_server.xml                                        |  38 
src/conversations/res/layout/activity_welcome.xml                                            |  41 
src/conversations/res/layout/dialog_enter_password.xml                                       |  81 
src/conversations/res/menu/easy_onboarding_invite.xml                                        |   2 
src/conversations/res/menu/manageaccounts.xml                                                |  53 
src/conversations/res/menu/welcome_menu.xml                                                  |   2 
src/conversations/res/values-ar/strings.xml                                                  |   2 
src/conversations/res/values-bg/strings.xml                                                  |   3 
src/conversations/res/values-da-rDK/strings.xml                                              |   3 
src/conversations/res/values-de/strings.xml                                                  |   3 
src/conversations/res/values-el/strings.xml                                                  |   3 
src/conversations/res/values-es/strings.xml                                                  |   4 
src/conversations/res/values-eu/strings.xml                                                  |   5 
src/conversations/res/values-fi/strings.xml                                                  |   3 
src/conversations/res/values-fr/strings.xml                                                  |   3 
src/conversations/res/values-gl/strings.xml                                                  |   3 
src/conversations/res/values-hr/strings.xml                                                  |   3 
src/conversations/res/values-hu/strings.xml                                                  |   3 
src/conversations/res/values-it/strings.xml                                                  |   2 
src/conversations/res/values-ja/strings.xml                                                  |   3 
src/conversations/res/values-nl/strings.xml                                                  |   8 
src/conversations/res/values-pl/strings.xml                                                  |   3 
src/conversations/res/values-pt-rBR/strings.xml                                              |   3 
src/conversations/res/values-ro-rRO/strings.xml                                              |   3 
src/conversations/res/values-ru/strings.xml                                                  |   3 
src/conversations/res/values-sk/strings.xml                                                  |   3 
src/conversations/res/values-sq/strings.xml                                                  |   2 
src/conversations/res/values-sr/strings.xml                                                  |   5 
src/conversations/res/values-szl/strings.xml                                                 |   3 
src/conversations/res/values-tr-rTR/strings.xml                                              |   3 
src/conversations/res/values-zh-rCN/strings.xml                                              |   2 
src/conversations/res/values/colors-themed.xml                                               |  63 
src/free/AndroidManifest.xml                                                                 |   5 
src/main/AndroidManifest.xml                                                                 |  28 
src/main/java/eu/siacs/conversations/AppSettings.java                                        | 143 
src/main/java/eu/siacs/conversations/Config.java                                             | 128 
src/main/java/eu/siacs/conversations/Conversations.java                                      | 116 
src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java                                |  11 
src/main/java/eu/siacs/conversations/crypto/TrustManagers.java                               |  17 
src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java                         |  29 
src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java                |  17 
src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java                            |   6 
src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java                          |  17 
src/main/java/eu/siacs/conversations/entities/Bookmark.java                                  |   7 
src/main/java/eu/siacs/conversations/entities/Contact.java                                   |  12 
src/main/java/eu/siacs/conversations/entities/Conversation.java                              |  81 
src/main/java/eu/siacs/conversations/entities/ListItem.java                                  |  10 
src/main/java/eu/siacs/conversations/entities/Presences.java                                 |   6 
src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java                          |  10 
src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java                        |  89 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java                              |  22 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java                         |  15 
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java                        |   3 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java                          |   3 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                               | 119 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java                            |  38 
src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java                    |   6 
src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java                        | 548 
src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java                    |   2 
src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java                     | 171 
src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java          |  28 
src/main/java/eu/siacs/conversations/services/BarcodeProvider.java                           |  10 
src/main/java/eu/siacs/conversations/services/CallIntegration.java                           | 577 
src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java          | 462 
src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java                   |  52 
src/main/java/eu/siacs/conversations/services/ExportBackupService.java                       |   9 
src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java                    |  13 
src/main/java/eu/siacs/conversations/services/MessageArchiveService.java                     |  14 
src/main/java/eu/siacs/conversations/services/NotificationService.java                       | 513 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java                     | 472 
src/main/java/eu/siacs/conversations/ui/AboutActivity.java                                   |  22 
src/main/java/eu/siacs/conversations/ui/AboutPreference.java                                 |  23 
src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java              |   8 
src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java                               |  18 
src/main/java/eu/siacs/conversations/ui/Activities.java                                      |  52 
src/main/java/eu/siacs/conversations/ui/BaseActivity.java                                    |  53 
src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java                              |   6 
src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java                               |   2 
src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java                          | 104 
src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java                        |  91 
src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java          |  27 
src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java                           |  30 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java                       | 107 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java                          | 101 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java                            |   6 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java                            | 117 
src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java                           |  46 
src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java                   |  16 
src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java                    |  12 
src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java                       |  25 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java                             | 128 
src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java                                  | 101 
src/main/java/eu/siacs/conversations/ui/ExtendedFabSizeChanger.java                          |  29 
src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java                            |   9 
src/main/java/eu/siacs/conversations/ui/LocationActivity.java                                |   7 
src/main/java/eu/siacs/conversations/ui/MediaBrowserActivity.java                            |   3 
src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java                              |  10 
src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java                                |   5 
src/main/java/eu/siacs/conversations/ui/OmemoActivity.java                                   |  20 
src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java          |   4 
src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java                   |  15 
src/main/java/eu/siacs/conversations/ui/RecordingActivity.java                               |  19 
src/main/java/eu/siacs/conversations/ui/RecyclerViews.java                                   |  29 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java                              | 311 
src/main/java/eu/siacs/conversations/ui/ScanActivity.java                                    |   1 
src/main/java/eu/siacs/conversations/ui/SearchActivity.java                                  |  20 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java                                | 730 
src/main/java/eu/siacs/conversations/ui/SettingsFragment.java                                | 104 
src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java                           |  39 
src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java                               | 122 
src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java                                |   2 
src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java                            | 186 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java                       | 100 
src/main/java/eu/siacs/conversations/ui/TimePreference.java                                  |   2 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java                               |  41 
src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java                              |  10 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                                    | 118 
src/main/java/eu/siacs/conversations/ui/activity/SettingsActivity.java                       |  70 
src/main/java/eu/siacs/conversations/ui/activity/result/PickRingtone.java                    |  47 
src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java                          |  28 
src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java              |  14 
src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java                     | 162 
src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java                       |  57 
src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java                         | 109 
src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java                            | 180 
src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java                     | 100 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java                          | 633 
src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java                             |  43 
src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java                      |  47 
src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java                   |  80 
src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java                          |  30 
src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java                          |  93 
src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java                 |  43 
src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java                      |  97 
src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java                               |  72 
src/main/java/eu/siacs/conversations/ui/fragment/settings/AttachmentsSettingsFragment.java   |  98 
src/main/java/eu/siacs/conversations/ui/fragment/settings/ConnectionSettingsFragment.java    |  73 
src/main/java/eu/siacs/conversations/ui/fragment/settings/InterfaceSettingsFragment.java     | 106 
src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java          | 107 
src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java | 161 
src/main/java/eu/siacs/conversations/ui/fragment/settings/PrivacySettingsFragment.java       |  46 
src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java      | 200 
src/main/java/eu/siacs/conversations/ui/fragment/settings/UpSettingsFragment.java            | 109 
src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java        |  96 
src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java                             | 171 
src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java                               |  35 
src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java                              |  88 
src/main/java/eu/siacs/conversations/ui/util/Attachment.java                                 |  87 
src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java               |   4 
src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java                |   5 
src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java                           |  10 
src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java                             | 174 
src/main/java/eu/siacs/conversations/ui/util/SettingsUtils.java                              |  17 
src/main/java/eu/siacs/conversations/ui/util/StyledAttributes.java                           |  59 
src/main/java/eu/siacs/conversations/ui/util/ToolbarUtils.java                               | 166 
src/main/java/eu/siacs/conversations/ui/util/UriHelper.java                                  |  15 
src/main/java/eu/siacs/conversations/ui/widget/ImmediateAutoCompleteTextView.java            |   4 
src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java                              |  19 
src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java                 |   5 
src/main/java/eu/siacs/conversations/ui/widget/UnreadCountCustomView.java                    |   9 
src/main/java/eu/siacs/conversations/utils/AccountUtils.java                                 |   6 
src/main/java/eu/siacs/conversations/utils/Compatibility.java                                |  82 
src/main/java/eu/siacs/conversations/utils/CryptoHelper.java                                 |  24 
src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java                              |  47 
src/main/java/eu/siacs/conversations/utils/GeoHelper.java                                    |  12 
src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java                     |   7 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                                    |   2 
src/main/java/eu/siacs/conversations/utils/SSLSockets.java                                   |  56 
src/main/java/eu/siacs/conversations/utils/StylingHelper.java                                |  44 
src/main/java/eu/siacs/conversations/utils/ThemeHelper.java                                  | 160 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                                     | 177 
src/main/java/eu/siacs/conversations/utils/WakeLockHelper.java                               |   8 
src/main/java/eu/siacs/conversations/utils/XEP0392Helper.java                                |   4 
src/main/java/eu/siacs/conversations/utils/XmlHelper.java                                    |  13 
src/main/java/eu/siacs/conversations/xml/Element.java                                        |  43 
src/main/java/eu/siacs/conversations/xml/Namespace.java                                      |   3 
src/main/java/eu/siacs/conversations/xml/Tag.java                                            |   4 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                                |  67 
src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java               |   8 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java                | 365 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java           |  29 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java                    | 261 
src/main/java/eu/siacs/conversations/xmpp/jingle/OngoingRtpSession.java                      |   9 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java                        |  35 
src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java                           |   8 
src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java                     |   3 
src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java                          |  61 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java                   |  11 
src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java                            |   1 
src/main/res/color/hint_on_tertiary_container.xml                                            |   3 
src/main/res/drawable-hdpi/baseline_tour_black_48.png                                        |   0 
src/main/res/drawable-hdpi/baseline_tour_white_48.png                                        |   0 
src/main/res/drawable-hdpi/date_bubble_grey.9.png                                            |   0 
src/main/res/drawable-hdpi/date_bubble_white.9.png                                           |   0 
src/main/res/drawable-hdpi/ic_account_box_white_24dp.png                                     |   0 
src/main/res/drawable-hdpi/ic_action_reply.png                                               |   0 
src/main/res/drawable-hdpi/ic_add_white_24dp.png                                             |   0 
src/main/res/drawable-hdpi/ic_android_black_48dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_android_white_48dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_announcement_white_24dp.png                                    |   0 
src/main/res/drawable-hdpi/ic_archive_black_48dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_archive_white_24dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_archive_white_48dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_attach_camera.png                                              |   0 
src/main/res/drawable-hdpi/ic_attach_camera_white.png                                        |   0 
src/main/res/drawable-hdpi/ic_attach_document.png                                            |   0 
src/main/res/drawable-hdpi/ic_attach_document_white.png                                      |   0 
src/main/res/drawable-hdpi/ic_attach_file_white_24dp.png                                     |   0 
src/main/res/drawable-hdpi/ic_attach_location.png                                            |   0 
src/main/res/drawable-hdpi/ic_attach_location_white.png                                      |   0 
src/main/res/drawable-hdpi/ic_attach_photo.png                                               |   0 
src/main/res/drawable-hdpi/ic_attach_photo_white.png                                         |   0 
src/main/res/drawable-hdpi/ic_attach_record.png                                              |   0 
src/main/res/drawable-hdpi/ic_attach_record_white.png                                        |   0 
src/main/res/drawable-hdpi/ic_attach_videocam.png                                            |   0 
src/main/res/drawable-hdpi/ic_attach_videocam_white.png                                      |   0 
src/main/res/drawable-hdpi/ic_autorenew_white_24dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_backup_black_48dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_backup_white_48dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_block_white_24dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_bluetooth_audio_black_24dp.png                                 |   0 
src/main/res/drawable-hdpi/ic_book_black_48dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_book_white_48dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_call_end_white_48dp.png                                        |   0 
src/main/res/drawable-hdpi/ic_call_made_black_18dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_call_made_white_18dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_call_missed_black_18dp.png                                     |   0 
src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png                            |   0 
src/main/res/drawable-hdpi/ic_call_missed_outgoing_white_18dp.png                            |   0 
src/main/res/drawable-hdpi/ic_call_missed_white_18dp.png                                     |   0 
src/main/res/drawable-hdpi/ic_call_received_black_18dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_call_received_white_18dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_call_white_48dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_camera_alt_white_24dp.png                                      |   0 
src/main/res/drawable-hdpi/ic_cancel_black_24dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_cancel_white_24dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_chat_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_clear_white_48dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png                                  |   0 
src/main/res/drawable-hdpi/ic_content_copy_white_24dp.png                                    |   0 
src/main/res/drawable-hdpi/ic_crop_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_delete_black_24dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_delete_white_24dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_description_black_48dp.png                                     |   0 
src/main/res/drawable-hdpi/ic_description_white_48dp.png                                     |   0 
src/main/res/drawable-hdpi/ic_directions_black_24dp.png                                      |   0 
src/main/res/drawable-hdpi/ic_directions_white_24dp.png                                      |   0 
src/main/res/drawable-hdpi/ic_done_black_18dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_done_white_18dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_drafts_white_24dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_edit_black_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_edit_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_error_white_24dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_event_black_48dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_event_white_48dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_file_download_white_24dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_flip_camera_android_black_24dp.png                             |   0 
src/main/res/drawable-hdpi/ic_forward_white_24dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_gps_fixed_white_24dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_gps_not_fixed_white_24dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_group_add_white_24dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_group_white_24dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_headset_black_24dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_headset_black_48dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_headset_white_48dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_help_black_48dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_help_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_help_white_48dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_hourglass_empty_white_24dp.png                                 |   0 
src/main/res/drawable-hdpi/ic_image_black_48dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_image_white_48dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_input_white_24dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_link_off_white_24dp.png                                        |   0 
src/main/res/drawable-hdpi/ic_link_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_lock_black_18dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_lock_white_18dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_lock_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_mic_black_24dp.png                                             |   0 
src/main/res/drawable-hdpi/ic_mic_black_48dp.png                                             |   0 
src/main/res/drawable-hdpi/ic_mic_off_black_24dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_mic_white_48dp.png                                             |   0 
src/main/res/drawable-hdpi/ic_mode_edit_black_18dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_mode_edit_white_18dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_new_releases_black_24dp.png                                    |   0 
src/main/res/drawable-hdpi/ic_new_releases_white_24dp.png                                    |   0 
src/main/res/drawable-hdpi/ic_no_results_background_black.png                                |   0 
src/main/res/drawable-hdpi/ic_no_results_background_white.png                                |   0 
src/main/res/drawable-hdpi/ic_notifications_black_24dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_notifications_none_black_24dp.png                              |   0 
src/main/res/drawable-hdpi/ic_notifications_none_white80.png                                 |   0 
src/main/res/drawable-hdpi/ic_notifications_none_white_24dp.png                              |   0 
src/main/res/drawable-hdpi/ic_notifications_off_black_24dp.png                               |   0 
src/main/res/drawable-hdpi/ic_notifications_off_white80.png                                  |   0 
src/main/res/drawable-hdpi/ic_notifications_off_white_24dp.png                               |   0 
src/main/res/drawable-hdpi/ic_notifications_paused_black_24dp.png                            |   0 
src/main/res/drawable-hdpi/ic_notifications_paused_white80.png                               |   0 
src/main/res/drawable-hdpi/ic_notifications_paused_white_24dp.png                            |   0 
src/main/res/drawable-hdpi/ic_notifications_white80.png                                      |   0 
src/main/res/drawable-hdpi/ic_notifications_white_24dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_pause_black_36dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_pause_white_36dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_person_add_white_24dp.png                                      |   0 
src/main/res/drawable-hdpi/ic_person_black_48dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_person_white_48dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_play_arrow_black_36dp.png                                      |   0 
src/main/res/drawable-hdpi/ic_play_arrow_white_36dp.png                                      |   0 
src/main/res/drawable-hdpi/ic_play_circle_filled_white_48dp.png                              |   0 
src/main/res/drawable-hdpi/ic_profile.png                                                    |   0 
src/main/res/drawable-hdpi/ic_public_white_24dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_qr_code_scan_white_24dp.png                                    |   0 
src/main/res/drawable-hdpi/ic_question_answer_white_24dp.png                                 |   0 
src/main/res/drawable-hdpi/ic_refresh_black_24dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_refresh_white_24dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_replay_white_48dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_reply_white_24dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_room_black_48dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_room_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_room_white_48dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_save_black_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_save_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_search_background_black.png                                    |   0 
src/main/res/drawable-hdpi/ic_search_background_white.png                                    |   0 
src/main/res/drawable-hdpi/ic_search_white_24dp.png                                          |   0 
src/main/res/drawable-hdpi/ic_send_cancel_away.png                                           |   0 
src/main/res/drawable-hdpi/ic_send_cancel_dnd.png                                            |   0 
src/main/res/drawable-hdpi/ic_send_cancel_offline.png                                        |   0 
src/main/res/drawable-hdpi/ic_send_cancel_offline_dark.png                                   |   0 
src/main/res/drawable-hdpi/ic_send_cancel_offline_white.png                                  |   0 
src/main/res/drawable-hdpi/ic_send_cancel_online.png                                         |   0 
src/main/res/drawable-hdpi/ic_send_file_offline.png                                          |   0 
src/main/res/drawable-hdpi/ic_send_file_offline_white.png                                    |   0 
src/main/res/drawable-hdpi/ic_send_location_away.png                                         |   0 
src/main/res/drawable-hdpi/ic_send_location_dnd.png                                          |   0 
src/main/res/drawable-hdpi/ic_send_location_offline.png                                      |   0 
src/main/res/drawable-hdpi/ic_send_location_offline_dark.png                                 |   0 
src/main/res/drawable-hdpi/ic_send_location_offline_white.png                                |   0 
src/main/res/drawable-hdpi/ic_send_location_online.png                                       |   0 
src/main/res/drawable-hdpi/ic_send_photo_away.png                                            |   0 
src/main/res/drawable-hdpi/ic_send_photo_dnd.png                                             |   0 
src/main/res/drawable-hdpi/ic_send_photo_offline.png                                         |   0 
src/main/res/drawable-hdpi/ic_send_photo_offline_dark.png                                    |   0 
src/main/res/drawable-hdpi/ic_send_photo_offline_white.png                                   |   0 
src/main/res/drawable-hdpi/ic_send_photo_online.png                                          |   0 
src/main/res/drawable-hdpi/ic_send_picture_away.png                                          |   0 
src/main/res/drawable-hdpi/ic_send_picture_dnd.png                                           |   0 
src/main/res/drawable-hdpi/ic_send_picture_offline.png                                       |   0 
src/main/res/drawable-hdpi/ic_send_picture_offline_dark.png                                  |   0 
src/main/res/drawable-hdpi/ic_send_picture_offline_white.png                                 |   0 
src/main/res/drawable-hdpi/ic_send_picture_online.png                                        |   0 
src/main/res/drawable-hdpi/ic_send_text_away.png                                             |   0 
src/main/res/drawable-hdpi/ic_send_text_dnd.png                                              |   0 
src/main/res/drawable-hdpi/ic_send_text_offline.png                                          |   0 
src/main/res/drawable-hdpi/ic_send_text_offline_dark.png                                     |   0 
src/main/res/drawable-hdpi/ic_send_text_offline_white.png                                    |   0 
src/main/res/drawable-hdpi/ic_send_text_online.png                                           |   0 
src/main/res/drawable-hdpi/ic_send_videocam_away.png                                         |   0 
src/main/res/drawable-hdpi/ic_send_videocam_dnd.png                                          |   0 
src/main/res/drawable-hdpi/ic_send_videocam_offline.png                                      |   0 
src/main/res/drawable-hdpi/ic_send_videocam_offline_white.png                                |   0 
src/main/res/drawable-hdpi/ic_send_videocam_online.png                                       |   0 
src/main/res/drawable-hdpi/ic_send_voice_away.png                                            |   0 
src/main/res/drawable-hdpi/ic_send_voice_dnd.png                                             |   0 
src/main/res/drawable-hdpi/ic_send_voice_offline.png                                         |   0 
src/main/res/drawable-hdpi/ic_send_voice_offline_dark.png                                    |   0 
src/main/res/drawable-hdpi/ic_send_voice_offline_white.png                                   |   0 
src/main/res/drawable-hdpi/ic_send_voice_online.png                                          |   0 
src/main/res/drawable-hdpi/ic_settings_black_24dp.png                                        |   0 
src/main/res/drawable-hdpi/ic_settings_white_24dp.png                                        |   0 
src/main/res/drawable-hdpi/ic_share_white_24dp.png                                           |   0 
src/main/res/drawable-hdpi/ic_star_black_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_star_white_24dp.png                                            |   0 
src/main/res/drawable-hdpi/ic_stat_alert_warning.png                                         |   0 
src/main/res/drawable-hdpi/ic_stat_communication_import_export.png                           |   0 
src/main/res/drawable-hdpi/ic_verified_fingerprint.png                                       |   0 
src/main/res/drawable-hdpi/ic_verified_user_black_18dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_verified_user_white_18dp.png                                   |   0 
src/main/res/drawable-hdpi/ic_videocam_black_24dp.png                                        |   0 
src/main/res/drawable-hdpi/ic_videocam_off_black_24dp.png                                    |   0 
src/main/res/drawable-hdpi/ic_videocam_white_24dp.png                                        |   0 
src/main/res/drawable-hdpi/ic_voicemail_white_24dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png                                      |   0 
src/main/res/drawable-hdpi/ic_volume_up_black_24dp.png                                       |   0 
src/main/res/drawable-hdpi/ic_warning_white_24dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_warning_white_48dp.png                                         |   0 
src/main/res/drawable-hdpi/ic_wear_reply.png                                                 |   0 
src/main/res/drawable-mdpi/baseline_tour_black_48.png                                        |   0 
src/main/res/drawable-mdpi/baseline_tour_white_48.png                                        |   0 
src/main/res/drawable-mdpi/date_bubble_grey.9.png                                            |   0 
src/main/res/drawable-mdpi/date_bubble_white.9.png                                           |   0 
src/main/res/drawable-mdpi/ic_account_box_white_24dp.png                                     |   0 
src/main/res/drawable-mdpi/ic_action_reply.png                                               |   0 
src/main/res/drawable-mdpi/ic_add_white_24dp.png                                             |   0 
src/main/res/drawable-mdpi/ic_android_black_48dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_android_white_48dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_announcement_white_24dp.png                                    |   0 
src/main/res/drawable-mdpi/ic_archive_black_48dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_archive_white_24dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_archive_white_48dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_attach_camera.png                                              |   0 
src/main/res/drawable-mdpi/ic_attach_camera_white.png                                        |   0 
src/main/res/drawable-mdpi/ic_attach_document.png                                            |   0 
src/main/res/drawable-mdpi/ic_attach_document_white.png                                      |   0 
src/main/res/drawable-mdpi/ic_attach_file_white_24dp.png                                     |   0 
src/main/res/drawable-mdpi/ic_attach_location.png                                            |   0 
src/main/res/drawable-mdpi/ic_attach_location_white.png                                      |   0 
src/main/res/drawable-mdpi/ic_attach_photo.png                                               |   0 
src/main/res/drawable-mdpi/ic_attach_photo_white.png                                         |   0 
src/main/res/drawable-mdpi/ic_attach_record.png                                              |   0 
src/main/res/drawable-mdpi/ic_attach_record_white.png                                        |   0 
src/main/res/drawable-mdpi/ic_attach_videocam.png                                            |   0 
src/main/res/drawable-mdpi/ic_attach_videocam_white.png                                      |   0 
src/main/res/drawable-mdpi/ic_autorenew_white_24dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_backup_black_48dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_backup_white_48dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_block_white_24dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_bluetooth_audio_black_24dp.png                                 |   0 
src/main/res/drawable-mdpi/ic_book_black_48dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_book_white_48dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_call_end_white_48dp.png                                        |   0 
src/main/res/drawable-mdpi/ic_call_made_black_18dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_call_made_white_18dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_call_missed_black_18dp.png                                     |   0 
src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png                            |   0 
src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png                            |   0 
src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png                                     |   0 
src/main/res/drawable-mdpi/ic_call_received_black_18dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_call_received_white_18dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_call_white_48dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_camera_alt_white_24dp.png                                      |   0 
src/main/res/drawable-mdpi/ic_cancel_black_24dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_cancel_white_24dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_chat_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_clear_white_48dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png                                  |   0 
src/main/res/drawable-mdpi/ic_content_copy_white_24dp.png                                    |   0 
src/main/res/drawable-mdpi/ic_crop_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_delete_black_24dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_delete_white_24dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_description_black_48dp.png                                     |   0 
src/main/res/drawable-mdpi/ic_description_white_48dp.png                                     |   0 
src/main/res/drawable-mdpi/ic_directions_black_24dp.png                                      |   0 
src/main/res/drawable-mdpi/ic_directions_white_24dp.png                                      |   0 
src/main/res/drawable-mdpi/ic_done_black_18dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_done_white_18dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_drafts_white_24dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_edit_black_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_edit_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_error_white_24dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_event_black_48dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_event_white_48dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_file_download_white_24dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_flip_camera_android_black_24dp.png                             |   0 
src/main/res/drawable-mdpi/ic_forward_white_24dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_gps_not_fixed_black_24dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_group_add_white_24dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_group_white_24dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_headset_black_24dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_headset_black_48dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_headset_white_48dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_help_black_48dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_help_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_help_white_48dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_hourglass_empty_white_24dp.png                                 |   0 
src/main/res/drawable-mdpi/ic_image_black_48dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_image_white_48dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_input_white_24dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_link_off_white_24dp.png                                        |   0 
src/main/res/drawable-mdpi/ic_link_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_lock_black_18dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_lock_white_18dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_lock_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_mic_black_24dp.png                                             |   0 
src/main/res/drawable-mdpi/ic_mic_black_48dp.png                                             |   0 
src/main/res/drawable-mdpi/ic_mic_off_black_24dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_mic_white_48dp.png                                             |   0 
src/main/res/drawable-mdpi/ic_mode_edit_black_18dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_mode_edit_white_18dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_new_releases_black_24dp.png                                    |   0 
src/main/res/drawable-mdpi/ic_new_releases_white_24dp.png                                    |   0 
src/main/res/drawable-mdpi/ic_no_results_background_black.png                                |   0 
src/main/res/drawable-mdpi/ic_no_results_background_white.png                                |   0 
src/main/res/drawable-mdpi/ic_notifications_black_24dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_notifications_none_black_24dp.png                              |   0 
src/main/res/drawable-mdpi/ic_notifications_none_white80.png                                 |   0 
src/main/res/drawable-mdpi/ic_notifications_none_white_24dp.png                              |   0 
src/main/res/drawable-mdpi/ic_notifications_off_black_24dp.png                               |   0 
src/main/res/drawable-mdpi/ic_notifications_off_white80.png                                  |   0 
src/main/res/drawable-mdpi/ic_notifications_off_white_24dp.png                               |   0 
src/main/res/drawable-mdpi/ic_notifications_paused_black_24dp.png                            |   0 
src/main/res/drawable-mdpi/ic_notifications_paused_white80.png                               |   0 
src/main/res/drawable-mdpi/ic_notifications_paused_white_24dp.png                            |   0 
src/main/res/drawable-mdpi/ic_notifications_white80.png                                      |   0 
src/main/res/drawable-mdpi/ic_notifications_white_24dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_pause_black_36dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_pause_white_36dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_person_add_white_24dp.png                                      |   0 
src/main/res/drawable-mdpi/ic_person_black_48dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_person_white_48dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_phone_in_talk_black_18dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_play_arrow_black_36dp.png                                      |   0 
src/main/res/drawable-mdpi/ic_play_arrow_white_36dp.png                                      |   0 
src/main/res/drawable-mdpi/ic_play_circle_filled_white_48dp.png                              |   0 
src/main/res/drawable-mdpi/ic_profile.png                                                    |   0 
src/main/res/drawable-mdpi/ic_public_white_24dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_qr_code_scan_white_24dp.png                                    |   0 
src/main/res/drawable-mdpi/ic_question_answer_white_24dp.png                                 |   0 
src/main/res/drawable-mdpi/ic_refresh_black_24dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_refresh_white_24dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_replay_white_48dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_reply_white_24dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_room_black_48dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_room_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_room_white_48dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_save_black_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_save_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_search_background_black.png                                    |   0 
src/main/res/drawable-mdpi/ic_search_background_white.png                                    |   0 
src/main/res/drawable-mdpi/ic_search_white_24dp.png                                          |   0 
src/main/res/drawable-mdpi/ic_send_cancel_away.png                                           |   0 
src/main/res/drawable-mdpi/ic_send_cancel_dnd.png                                            |   0 
src/main/res/drawable-mdpi/ic_send_cancel_offline.png                                        |   0 
src/main/res/drawable-mdpi/ic_send_cancel_offline_dark.png                                   |   0 
src/main/res/drawable-mdpi/ic_send_cancel_offline_white.png                                  |   0 
src/main/res/drawable-mdpi/ic_send_cancel_online.png                                         |   0 
src/main/res/drawable-mdpi/ic_send_file_offline.png                                          |   0 
src/main/res/drawable-mdpi/ic_send_file_offline_white.png                                    |   0 
src/main/res/drawable-mdpi/ic_send_location_away.png                                         |   0 
src/main/res/drawable-mdpi/ic_send_location_dnd.png                                          |   0 
src/main/res/drawable-mdpi/ic_send_location_offline.png                                      |   0 
src/main/res/drawable-mdpi/ic_send_location_offline_dark.png                                 |   0 
src/main/res/drawable-mdpi/ic_send_location_offline_white.png                                |   0 
src/main/res/drawable-mdpi/ic_send_location_online.png                                       |   0 
src/main/res/drawable-mdpi/ic_send_photo_away.png                                            |   0 
src/main/res/drawable-mdpi/ic_send_photo_dnd.png                                             |   0 
src/main/res/drawable-mdpi/ic_send_photo_offline.png                                         |   0 
src/main/res/drawable-mdpi/ic_send_photo_offline_dark.png                                    |   0 
src/main/res/drawable-mdpi/ic_send_photo_offline_white.png                                   |   0 
src/main/res/drawable-mdpi/ic_send_photo_online.png                                          |   0 
src/main/res/drawable-mdpi/ic_send_picture_away.png                                          |   0 
src/main/res/drawable-mdpi/ic_send_picture_dnd.png                                           |   0 
src/main/res/drawable-mdpi/ic_send_picture_offline.png                                       |   0 
src/main/res/drawable-mdpi/ic_send_picture_offline_dark.png                                  |   0 
src/main/res/drawable-mdpi/ic_send_picture_offline_white.png                                 |   0 
src/main/res/drawable-mdpi/ic_send_picture_online.png                                        |   0 
src/main/res/drawable-mdpi/ic_send_text_away.png                                             |   0 
src/main/res/drawable-mdpi/ic_send_text_dnd.png                                              |   0 
src/main/res/drawable-mdpi/ic_send_text_offline.png                                          |   0 
src/main/res/drawable-mdpi/ic_send_text_offline_dark.png                                     |   0 
src/main/res/drawable-mdpi/ic_send_text_offline_white.png                                    |   0 
src/main/res/drawable-mdpi/ic_send_text_online.png                                           |   0 
src/main/res/drawable-mdpi/ic_send_videocam_away.png                                         |   0 
src/main/res/drawable-mdpi/ic_send_videocam_dnd.png                                          |   0 
src/main/res/drawable-mdpi/ic_send_videocam_offline.png                                      |   0 
src/main/res/drawable-mdpi/ic_send_videocam_offline_white.png                                |   0 
src/main/res/drawable-mdpi/ic_send_videocam_online.png                                       |   0 
src/main/res/drawable-mdpi/ic_send_voice_away.png                                            |   0 
src/main/res/drawable-mdpi/ic_send_voice_dnd.png                                             |   0 
src/main/res/drawable-mdpi/ic_send_voice_offline.png                                         |   0 
src/main/res/drawable-mdpi/ic_send_voice_offline_dark.png                                    |   0 
src/main/res/drawable-mdpi/ic_send_voice_offline_white.png                                   |   0 
src/main/res/drawable-mdpi/ic_send_voice_online.png                                          |   0 
src/main/res/drawable-mdpi/ic_settings_black_24dp.png                                        |   0 
src/main/res/drawable-mdpi/ic_settings_white_24dp.png                                        |   0 
src/main/res/drawable-mdpi/ic_share_white_24dp.png                                           |   0 
src/main/res/drawable-mdpi/ic_star_black_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_star_white_24dp.png                                            |   0 
src/main/res/drawable-mdpi/ic_stat_alert_warning.png                                         |   0 
src/main/res/drawable-mdpi/ic_stat_communication_import_export.png                           |   0 
src/main/res/drawable-mdpi/ic_verified_fingerprint.png                                       |   0 
src/main/res/drawable-mdpi/ic_verified_user_black_18dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_verified_user_white_18dp.png                                   |   0 
src/main/res/drawable-mdpi/ic_videocam_black_24dp.png                                        |   0 
src/main/res/drawable-mdpi/ic_videocam_off_black_24dp.png                                    |   0 
src/main/res/drawable-mdpi/ic_videocam_white_24dp.png                                        |   0 
src/main/res/drawable-mdpi/ic_voicemail_white_24dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png                                      |   0 
src/main/res/drawable-mdpi/ic_volume_up_black_24dp.png                                       |   0 
src/main/res/drawable-mdpi/ic_warning_white_24dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_warning_white_48dp.png                                         |   0 
src/main/res/drawable-mdpi/ic_wear_reply.png                                                 |   0 
src/main/res/drawable-mdpi/play_gif_black.png                                                |   0 
src/main/res/drawable-mdpi/play_gif_white.png                                                |   0 
src/main/res/drawable-mdpi/play_video_black.png                                              |   0 
src/main/res/drawable-mdpi/play_video_white.png                                              |   0 
src/main/res/drawable-xhdpi/baseline_tour_black_48.png                                       |   0 
src/main/res/drawable-xhdpi/baseline_tour_white_48.png                                       |   0 
src/main/res/drawable-xhdpi/date_bubble_grey.9.png                                           |   0 
src/main/res/drawable-xhdpi/date_bubble_white.9.png                                          |   0 
src/main/res/drawable-xhdpi/ic_account_box_white_24dp.png                                    |   0 
src/main/res/drawable-xhdpi/ic_action_reply.png                                              |   0 
src/main/res/drawable-xhdpi/ic_add_white_24dp.png                                            |   0 
src/main/res/drawable-xhdpi/ic_android_black_48dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_android_white_48dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_announcement_white_24dp.png                                   |   0 
src/main/res/drawable-xhdpi/ic_archive_black_48dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_archive_white_24dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_archive_white_48dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_attach_camera.png                                             |   0 
src/main/res/drawable-xhdpi/ic_attach_camera_white.png                                       |   0 
src/main/res/drawable-xhdpi/ic_attach_document.png                                           |   0 
src/main/res/drawable-xhdpi/ic_attach_document_white.png                                     |   0 
src/main/res/drawable-xhdpi/ic_attach_file_white_24dp.png                                    |   0 
src/main/res/drawable-xhdpi/ic_attach_location.png                                           |   0 
src/main/res/drawable-xhdpi/ic_attach_location_white.png                                     |   0 
src/main/res/drawable-xhdpi/ic_attach_photo.png                                              |   0 
src/main/res/drawable-xhdpi/ic_attach_photo_white.png                                        |   0 
src/main/res/drawable-xhdpi/ic_attach_record.png                                             |   0 
src/main/res/drawable-xhdpi/ic_attach_record_white.png                                       |   0 
src/main/res/drawable-xhdpi/ic_attach_videocam.png                                           |   0 
src/main/res/drawable-xhdpi/ic_attach_videocam_white.png                                     |   0 
src/main/res/drawable-xhdpi/ic_autorenew_white_24dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_backup_black_48dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_backup_white_48dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_block_white_24dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_bluetooth_audio_black_24dp.png                                |   0 
src/main/res/drawable-xhdpi/ic_book_black_48dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_book_white_48dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_call_end_white_48dp.png                                       |   0 
src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png                                    |   0 
src/main/res/drawable-xhdpi/ic_call_missed_outgoing_black_18dp.png                           |   0 
src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png                           |   0 
src/main/res/drawable-xhdpi/ic_call_missed_white_18dp.png                                    |   0 
src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_call_received_white_18dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_call_white_48dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_camera_alt_white_24dp.png                                     |   0 
src/main/res/drawable-xhdpi/ic_cancel_black_24dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_cancel_white_24dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_chat_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_clear_white_48dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png                                 |   0 
src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.png                                   |   0 
src/main/res/drawable-xhdpi/ic_crop_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_delete_black_24dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_delete_white_24dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_description_black_48dp.png                                    |   0 
src/main/res/drawable-xhdpi/ic_description_white_48dp.png                                    |   0 
src/main/res/drawable-xhdpi/ic_directions_black_24dp.png                                     |   0 
src/main/res/drawable-xhdpi/ic_directions_white_24dp.png                                     |   0 
src/main/res/drawable-xhdpi/ic_done_black_18dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_done_white_18dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_drafts_white_24dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_edit_black_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_edit_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_error_white_24dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_event_black_48dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_event_white_48dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_flip_camera_android_black_24dp.png                            |   0 
src/main/res/drawable-xhdpi/ic_forward_white_24dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_gps_not_fixed_white_24dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_group_add_white_24dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_group_white_24dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_headset_black_24dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_headset_black_48dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_headset_white_48dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_help_black_48dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_help_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_help_white_48dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_hourglass_empty_white_24dp.png                                |   0 
src/main/res/drawable-xhdpi/ic_image_black_48dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_image_white_48dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_input_white_24dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_link_off_white_24dp.png                                       |   0 
src/main/res/drawable-xhdpi/ic_link_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_lock_black_18dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_lock_white_18dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_lock_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_mic_black_24dp.png                                            |   0 
src/main/res/drawable-xhdpi/ic_mic_black_48dp.png                                            |   0 
src/main/res/drawable-xhdpi/ic_mic_off_black_24dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_mic_white_48dp.png                                            |   0 
src/main/res/drawable-xhdpi/ic_mode_edit_black_18dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_mode_edit_white_18dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_new_releases_black_24dp.png                                   |   0 
src/main/res/drawable-xhdpi/ic_new_releases_white_24dp.png                                   |   0 
src/main/res/drawable-xhdpi/ic_no_results_background_black.png                               |   0 
src/main/res/drawable-xhdpi/ic_no_results_background_white.png                               |   0 
src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_notifications_none_black_24dp.png                             |   0 
src/main/res/drawable-xhdpi/ic_notifications_none_white80.png                                |   0 
src/main/res/drawable-xhdpi/ic_notifications_none_white_24dp.png                             |   0 
src/main/res/drawable-xhdpi/ic_notifications_off_black_24dp.png                              |   0 
src/main/res/drawable-xhdpi/ic_notifications_off_white80.png                                 |   0 
src/main/res/drawable-xhdpi/ic_notifications_off_white_24dp.png                              |   0 
src/main/res/drawable-xhdpi/ic_notifications_paused_black_24dp.png                           |   0 
src/main/res/drawable-xhdpi/ic_notifications_paused_white80.png                              |   0 
src/main/res/drawable-xhdpi/ic_notifications_paused_white_24dp.png                           |   0 
src/main/res/drawable-xhdpi/ic_notifications_white80.png                                     |   0 
src/main/res/drawable-xhdpi/ic_notifications_white_24dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_pause_black_36dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_pause_white_36dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_person_add_white_24dp.png                                     |   0 
src/main/res/drawable-xhdpi/ic_person_black_48dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_person_white_48dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_phone_in_talk_white_18dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_phone_in_talk_white_24dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_play_arrow_black_36dp.png                                     |   0 
src/main/res/drawable-xhdpi/ic_play_arrow_white_36dp.png                                     |   0 
src/main/res/drawable-xhdpi/ic_play_circle_filled_white_48dp.png                             |   0 
src/main/res/drawable-xhdpi/ic_profile.png                                                   |   0 
src/main/res/drawable-xhdpi/ic_public_white_24dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_qr_code_scan_white_24dp.png                                   |   0 
src/main/res/drawable-xhdpi/ic_question_answer_white_24dp.png                                |   0 
src/main/res/drawable-xhdpi/ic_refresh_black_24dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_replay_white_48dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_reply_white_24dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_room_black_48dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_room_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_room_white_48dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_save_black_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_save_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_search_background_black.png                                   |   0 
src/main/res/drawable-xhdpi/ic_search_background_white.png                                   |   0 
src/main/res/drawable-xhdpi/ic_search_white_24dp.png                                         |   0 
src/main/res/drawable-xhdpi/ic_send_cancel_away.png                                          |   0 
src/main/res/drawable-xhdpi/ic_send_cancel_dnd.png                                           |   0 
src/main/res/drawable-xhdpi/ic_send_cancel_offline.png                                       |   0 
src/main/res/drawable-xhdpi/ic_send_cancel_offline_dark.png                                  |   0 
src/main/res/drawable-xhdpi/ic_send_cancel_offline_white.png                                 |   0 
src/main/res/drawable-xhdpi/ic_send_cancel_online.png                                        |   0 
src/main/res/drawable-xhdpi/ic_send_file_offline.png                                         |   0 
src/main/res/drawable-xhdpi/ic_send_file_offline_white.png                                   |   0 
src/main/res/drawable-xhdpi/ic_send_location_away.png                                        |   0 
src/main/res/drawable-xhdpi/ic_send_location_dnd.png                                         |   0 
src/main/res/drawable-xhdpi/ic_send_location_offline.png                                     |   0 
src/main/res/drawable-xhdpi/ic_send_location_offline_dark.png                                |   0 
src/main/res/drawable-xhdpi/ic_send_location_offline_white.png                               |   0 
src/main/res/drawable-xhdpi/ic_send_location_online.png                                      |   0 
src/main/res/drawable-xhdpi/ic_send_photo_away.png                                           |   0 
src/main/res/drawable-xhdpi/ic_send_photo_dnd.png                                            |   0 
src/main/res/drawable-xhdpi/ic_send_photo_offline.png                                        |   0 
src/main/res/drawable-xhdpi/ic_send_photo_offline_dark.png                                   |   0 
src/main/res/drawable-xhdpi/ic_send_photo_offline_white.png                                  |   0 
src/main/res/drawable-xhdpi/ic_send_photo_online.png                                         |   0 
src/main/res/drawable-xhdpi/ic_send_picture_away.png                                         |   0 
src/main/res/drawable-xhdpi/ic_send_picture_dnd.png                                          |   0 
src/main/res/drawable-xhdpi/ic_send_picture_offline.png                                      |   0 
src/main/res/drawable-xhdpi/ic_send_picture_offline_dark.png                                 |   0 
src/main/res/drawable-xhdpi/ic_send_picture_offline_white.png                                |   0 
src/main/res/drawable-xhdpi/ic_send_picture_online.png                                       |   0 
src/main/res/drawable-xhdpi/ic_send_text_away.png                                            |   0 
src/main/res/drawable-xhdpi/ic_send_text_dnd.png                                             |   0 
src/main/res/drawable-xhdpi/ic_send_text_offline.png                                         |   0 
src/main/res/drawable-xhdpi/ic_send_text_offline_dark.png                                    |   0 
src/main/res/drawable-xhdpi/ic_send_text_offline_white.png                                   |   0 
src/main/res/drawable-xhdpi/ic_send_text_online.png                                          |   0 
src/main/res/drawable-xhdpi/ic_send_videocam_away.png                                        |   0 
src/main/res/drawable-xhdpi/ic_send_videocam_dnd.png                                         |   0 
src/main/res/drawable-xhdpi/ic_send_videocam_offline.png                                     |   0 
src/main/res/drawable-xhdpi/ic_send_videocam_offline_white.png                               |   0 
src/main/res/drawable-xhdpi/ic_send_videocam_online.png                                      |   0 
src/main/res/drawable-xhdpi/ic_send_voice_away.png                                           |   0 
src/main/res/drawable-xhdpi/ic_send_voice_dnd.png                                            |   0 
src/main/res/drawable-xhdpi/ic_send_voice_offline.png                                        |   0 
src/main/res/drawable-xhdpi/ic_send_voice_offline_dark.png                                   |   0 
src/main/res/drawable-xhdpi/ic_send_voice_offline_white.png                                  |   0 
src/main/res/drawable-xhdpi/ic_send_voice_online.png                                         |   0 
src/main/res/drawable-xhdpi/ic_settings_black_24dp.png                                       |   0 
src/main/res/drawable-xhdpi/ic_settings_white_24dp.png                                       |   0 
src/main/res/drawable-xhdpi/ic_share_white_24dp.png                                          |   0 
src/main/res/drawable-xhdpi/ic_star_black_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_star_white_24dp.png                                           |   0 
src/main/res/drawable-xhdpi/ic_stat_alert_warning.png                                        |   0 
src/main/res/drawable-xhdpi/ic_stat_communication_import_export.png                          |   0 
src/main/res/drawable-xhdpi/ic_verified_fingerprint.png                                      |   0 
src/main/res/drawable-xhdpi/ic_verified_user_black_18dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_verified_user_white_18dp.png                                  |   0 
src/main/res/drawable-xhdpi/ic_videocam_black_24dp.png                                       |   0 
src/main/res/drawable-xhdpi/ic_videocam_off_black_24dp.png                                   |   0 
src/main/res/drawable-xhdpi/ic_videocam_white_24dp.png                                       |   0 
src/main/res/drawable-xhdpi/ic_voicemail_white_24dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png                                     |   0 
src/main/res/drawable-xhdpi/ic_volume_up_black_24dp.png                                      |   0 
src/main/res/drawable-xhdpi/ic_warning_white_24dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_warning_white_48dp.png                                        |   0 
src/main/res/drawable-xhdpi/ic_wear_reply.png                                                |   0 
src/main/res/drawable-xxhdpi/baseline_tour_black_48.png                                      |   0 
src/main/res/drawable-xxhdpi/baseline_tour_white_48.png                                      |   0 
src/main/res/drawable-xxhdpi/date_bubble_grey.9.png                                          |   0 
src/main/res/drawable-xxhdpi/date_bubble_white.9.png                                         |   0 
src/main/res/drawable-xxhdpi/ic_account_box_white_24dp.png                                   |   0 
src/main/res/drawable-xxhdpi/ic_action_reply.png                                             |   0 
src/main/res/drawable-xxhdpi/ic_add_white_24dp.png                                           |   0 
src/main/res/drawable-xxhdpi/ic_android_black_48dp.png                                       |   0 
src/main/res/drawable-xxhdpi/ic_android_white_48dp.png                                       |   0 
src/main/res/drawable-xxhdpi/ic_announcement_white_24dp.png                                  |   0 
src/main/res/drawable-xxhdpi/ic_archive_black_48dp.png                                       |   0 
src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png                                       |   0 
src/main/res/drawable-xxhdpi/ic_archive_white_48dp.png                                       |   0 
src/main/res/drawable-xxhdpi/ic_attach_camera.png                                            |   0 
src/main/res/drawable-xxhdpi/ic_attach_camera_white.png                                      |   0 
src/main/res/drawable-xxhdpi/ic_attach_document.png                                          |   0 
src/main/res/drawable-xxhdpi/ic_attach_document_white.png                                    |   0 
src/main/res/drawable-xxhdpi/ic_attach_file_white_24dp.png                                   |   0 
src/main/res/drawable-xxhdpi/ic_attach_location.png                                          |   0 
src/main/res/drawable-xxhdpi/ic_attach_location_white.png                                    |   0 
src/main/res/drawable-xxhdpi/ic_attach_photo.png                                             |   0 
src/main/res/drawable-xxhdpi/ic_attach_photo_white.png                                       |   0 
src/main/res/drawable-xxhdpi/ic_attach_record.png                                            |   0 
src/main/res/drawable-xxhdpi/ic_attach_record_white.png                                      |   0 
src/main/res/drawable-xxhdpi/ic_attach_videocam.png                                          |   0 
src/main/res/drawable-xxhdpi/ic_attach_videocam_white.png                                    |   0 
src/main/res/drawable-xxhdpi/ic_autorenew_white_24dp.png                                     |   0 
src/main/res/drawable-xxhdpi/ic_backup_black_48dp.png                                        |   0 
src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png                                        |   0 
src/main/res/drawable-xxhdpi/ic_block_white_24dp.png                                         |   0 
src/main/res/drawable-xxhdpi/ic_bluetooth_audio_black_24dp.png                               |   0 
src/main/res/drawable-xxhdpi/ic_book_black_48dp.png                                          |   0 
src/main/res/drawable-xxhdpi/ic_book_white_48dp.png                                          |   0 
src/main/res/drawable-xxhdpi/ic_call_end_white_48dp.png                                      |   0 
src/main/res/drawable-xxhdpi/ic_call_made_black_18dp.png                                     |   0 
src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png                                     |   0 
1,000 files changed, 8,639 insertions(+), 6,467 deletions(-)

Detailed changes

CHANGELOG.md 🔗

@@ -1,5 +1,41 @@
 # Changelog
 
+### Version 2.15.3
+
+* fix call integration on some Android 14 devices
+* Introduce 'Invites from Strangers' setting
+
+### Version 2.15.2
+
+* Fix Quicksy registration on Android 6/7
+* Play incoming call ringtone on notification channel
+
+### Version 2.15.1
+
+* Show message status as icons
+* Introduce 'Large font' setting for message bubbles
+
+### Version 2.15.0
+
+* Use Material 3 theme
+* Reorganize settings
+* Synchronize read state across devices
+
+### Version 2.14.2
+
+* Restore access to Channel Discovery for Android 6+7
+* Improve logging for failed call integration
+
+### Version 2.14.1
+
+* Fix A/V calls on Android 8
+* Fix race conditions in new call integration
+* Fix video compression sticking around
+
+### Version 2.14.0
+
+* Improve integration of A/V calls into the operating system
+
 ### Version 2.13.4
 
 * Fix minor regressions introduced with 2.13.1

build.gradle 🔗

@@ -6,7 +6,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:8.2.0-rc03'
+        classpath 'com.android.tools.build:gradle:8.3.1'
     }
 }
 
@@ -52,7 +52,7 @@ dependencies {
 
     implementation 'androidx.viewpager:viewpager:1.0.0'
 
-    playstoreImplementation('com.google.firebase:firebase-messaging:23.4.0') {
+    playstoreImplementation('com.google.firebase:firebase-messaging:23.4.1') {
         exclude group: 'com.google.firebase', module: 'firebase-core'
         exclude group: 'com.google.firebase', module: 'firebase-analytics'
         exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@@ -60,12 +60,13 @@ dependencies {
     cheogramPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2")
     cheogramPlaystoreImplementation 'com.github.singpolyma:play-licensing:1c637ea03c'
     conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2")
-    quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
+    quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.2'
     implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1'
     implementation("com.github.CanHub:Android-Image-Cropper:2.0.0")
     implementation 'androidx.appcompat:appcompat:1.6.1'
     implementation 'androidx.exifinterface:exifinterface:1.3.7'
     implementation 'androidx.cardview:cardview:1.0.0'
+    implementation "androidx.preference:preference:1.2.1"
     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
     implementation 'com.google.android.material:material:1.11.0'
 
@@ -78,8 +79,9 @@ dependencies {
     implementation 'com.google.zxing:core:3.3.3'
     implementation 'org.minidns:minidns-hla:1.0.4'
     implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
-    implementation 'org.whispersystems:signal-protocol-android:2.6.2'
+    implementation 'org.whispersystems:signal-protocol-java:2.6.2'
     implementation "com.wefika:flowlayout:0.4.1"
+
     //noinspection GradleDependency
     implementation('com.github.natario1:Transcoder:v0.9.1') {
         exclude group: 'com.otaliastudios.opengl', module: 'egloo'
@@ -92,24 +94,24 @@ dependencies {
     implementation 'org.hsluv:hsluv:0.2'
     implementation 'org.conscrypt:conscrypt-android:2.5.2'
     implementation 'me.drakeet.support:toastcompat:1.1.0'
-    implementation "com.leinardi.android:speed-dial:3.2.0"
+    implementation "com.leinardi.android:speed-dial:3.3.0"
 
     implementation "com.squareup.retrofit2:retrofit:2.9.0"
     implementation "com.squareup.retrofit2:converter-gson:2.9.0"
     implementation "com.squareup.okhttp3:okhttp:4.12.0"
 
     implementation 'com.google.guava:guava:32.1.3-android'
-    implementation 'io.michaelrocks:libphonenumber-android:8.13.17'
+    implementation 'io.michaelrocks:libphonenumber-android:8.13.28'
+    implementation 'im.conversations.webrtc:webrtc-android:119.0.1'
     implementation 'io.github.nishkarsh:android-permissions:2.1.6'
     implementation 'androidx.recyclerview:recyclerview:1.1.0'
     implementation 'androidx.documentfile:documentfile:1.0.1'
-    implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:2.4.2'
+    implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0'
     implementation 'com.github.ipld:java-cid:v1.3.1'
     //implementation 'com.splitwise:tokenautocomplete:3.0.2'
     implementation 'com.github.singpolyma:TokenAutoComplete:bfa93780e0'
     implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4'
     implementation 'com.github.singpolyma:android-identicons:3361281bd4'
-    implementation 'im.conversations.webrtc:webrtc-android:119.0.0'
     implementation 'com.github.woltapp:blurhash:master'
     implementation 'com.caverock:androidsvg-aar:1.4'
     implementation 'org.tomlj:tomlj:1.1.0'
@@ -128,7 +130,7 @@ android {
     compileSdk 34
 
     defaultConfig {
-        minSdkVersion 21
+        minSdkVersion 23
         targetSdkVersion 34
         versionCode 42025 + tags.size()
         versionName grgit.describe(always: true)

fastlane/metadata/android/ru-RU/changelogs/4209204.txt 🔗

@@ -0,0 +1,2 @@
+* Упрощён доступ к Политике конфиденциальности в версии для Play Маркета (Quicksy и Conversations)
+* Из версии Conversations для Play Маркета удалена интеграция с Контактами

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

@@ -1,8 +1,8 @@
 Version 2.10.9
-* Kërko leje Bluetooth, kur bëhen thirrje A/V (Mund ta hidhni tej këtë, nëse s’përdorni kufje Bluetooth me mikrofon)
-* Ndreqje të mete, kur thirret dikush në Movim
-* Ndreqje shfaqjeje avatari gabim për fjalosje në grup
+* Kërko leje Bluetooth, kur bëhen thirrje A/V (Mund ta hidhni tej, nëse s’përdorni kufje Bluetooth me mikrofon)
+* Ndreqje të mete, për thirrje në Movim
+* Ndreqje shfaqjeje avatari të gabuar për fjalosje në grup
 * Pyet përherë për lënie jashtë optimizimesh për baterinë
 * Vendosje flamurke “vetëm vendore” për njoftime “x llogari të lidhura”
-* Ndreqje ndërveprimi me shtojcën Google Maps Share Location Plugin
-* Heqje poshtëshënimi lidhur me tarifa shërbyesi
+* Ndreqje ndërveprimi me shtojcën Google Maps Share Location
+* Heqje poshtëshënimi lidhur me tarifë shërbyesi

fastlane/metadata/android/sq/changelogs/4208104.txt 🔗

@@ -0,0 +1,4 @@
+* Përdorim më i lehtë i “Shfaq kod QR”
+* Mbulim për Faqerojtës PEP të vetëm sistemit
+* Shtim mbulimi për Ofertë SDP / Model Përgjigjesh (Përdorur nga kanale SIP)
+* Ngritje e versioni të synuar API në Android 14

fastlane/metadata/android/sq/changelogs/4208804.txt 🔗

@@ -0,0 +1,3 @@
+* Mbulim shpërnguljesh kartelash P2P përmes kanalesh WebRTC të dhënash
+* Ndreqje problemesh shkalle ndërveprimi me Bind 2.0 në ejabberd
+* Paketim dëshmish Let’s Encrypt rrënje për Android <= 7

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

@@ -0,0 +1,3 @@
+* Виправлення голосових та відеовикликів на Android 8
+* Виправлення проблем у новій інтеграції дзвінків
+* Виправлення проблем зі стисненням відео

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

@@ -0,0 +1,2 @@
+* Відновлено доступ до пошуку каналів на Android 6+7
+* Покращено ведення журналу для невдалої інтеграції викликів

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

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

gradle/wrapper/gradle-wrapper.properties 🔗

@@ -1,6 +1,8 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=5022b0b25fe182b0e50867e77f484501dba44feeea88f5c1f13b6b4660463640
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip

gradlew 🔗

@@ -55,7 +55,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,11 @@ do
     esac
 done
 
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
@@ -133,22 +131,29 @@ location of your Java installation."
     fi
 else
     JAVACMD=java
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
 if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
     case $MAX_FD in  #(
       '' | soft) :;; #(
       *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
@@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
     done
 fi
 
-# Collect all arguments for the java command;
-#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-#     shell script including quotes and variable substitutions, so put them in
-#     double quotes to make sure that they get re-expanded; and
-#   * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -205,6 +214,12 @@ set -- \
         org.gradle.wrapper.GradleWrapperMain \
         "$@"
 
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
 # Use "xargs" to parse quoted args.
 #
 # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

gradlew.bat 🔗

@@ -14,7 +14,7 @@
 @rem limitations under the License.
 @rem
 
-@if "%DEBUG%" == "" @echo off

+@if "%DEBUG%"=="" @echo off

 @rem ##########################################################################
 @rem
 @rem  Gradle startup script for Windows
@@ -25,7 +25,8 @@
 if "%OS%"=="Windows_NT" setlocal
 
 set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.

+if "%DIRNAME%"=="" set DIRNAME=.

+@rem This is normally unused

 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
@@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute

+if %ERRORLEVEL% equ 0 goto execute

 
-echo.

-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

-echo.

-echo Please set the JAVA_HOME variable in your environment to match the

-echo location of your Java installation.

+echo. 1>&2

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2

+echo. 1>&2

+echo Please set the JAVA_HOME variable in your environment to match the 1>&2

+echo location of your Java installation. 1>&2

 
 goto fail
 
@@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
 if exist "%JAVA_EXE%" goto execute
 
-echo.

-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

-echo.

-echo Please set the JAVA_HOME variable in your environment to match the

-echo location of your Java installation.

+echo. 1>&2

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2

+echo. 1>&2

+echo Please set the JAVA_HOME variable in your environment to match the 1>&2

+echo location of your Java installation. 1>&2

 
 goto fail
 
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
 :end
 @rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd

+if %ERRORLEVEL% equ 0 goto mainEnd

 
 :fail
 rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
 rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

-exit /b 1

+set EXIT_CODE=%ERRORLEVEL%

+if %EXIT_CODE% equ 0 set EXIT_CODE=1

+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%

+exit /b %EXIT_CODE%

 
 :mainEnd
 if "%OS%"=="Windows_NT" endlocal

proguard-rules.pro 🔗

@@ -34,6 +34,7 @@
 -dontwarn org.openjsse.javax.net.ssl.SSLParameters
 -dontwarn org.openjsse.javax.net.ssl.SSLSocket
 -dontwarn org.openjsse.net.ssl.OpenJSSE
+-dontwarn org.jetbrains.annotations.**
 
 -keepclassmembers class eu.siacs.conversations.http.services.** {
   !transient <fields>;

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

@@ -45,7 +45,7 @@ import io.michaelrocks.libphonenumber.android.NumberParseException;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.services.AppRTCAudioManager;
+import eu.siacs.conversations.services.CallIntegration;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.EventReceiver;
 import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
@@ -120,7 +120,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
 			);
 		}
 
-		if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) {
+		if (xmppConnectionService.getJingleConnectionManager().isBusy()) {
 			return Connection.createFailedConnection(
 				new DisconnectCause(DisconnectCause.BUSY)
 			);
@@ -154,7 +154,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
 					account,
 					with,
 					ImmutableSet.of(Media.AUDIO)
-				));
+				).sessionId);
 			}
 
 			@Override
@@ -245,6 +245,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
 				Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION |
 				Connection.CAPABILITY_MUTE
 			);
+        setRingbackRequested(true);
 		}
 
 		public void setSessionId(final String sessionId) {
@@ -290,7 +291,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
 		}
 
 		@Override
-		public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
+		public void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
 			if (Build.VERSION.SDK_INT < 26) return;
 
 			if (pendingState != null) {
@@ -322,29 +323,13 @@ public class ConnectionService extends android.telecom.ConnectionService {
 		@Override
 		public void onCallAudioStateChanged(CallAudioState state) {
 			pendingState = null;
-			if (rtpConnection == null || rtpConnection.get() == null || rtpConnection.get().getAudioManager() == null) {
+			if (rtpConnection == null || rtpConnection.get() == null) {
 				pendingState = state;
 				return;
 			}
 
 			Log.d("com.cheogram.android.CheogramConnection", "onCallAudioStateChanged: " + state);
-
-			switch(state.getRoute()) {
-				case CallAudioState.ROUTE_SPEAKER:
-					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
-					break;
-				case CallAudioState.ROUTE_WIRED_HEADSET:
-					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.WIRED_HEADSET);
-					break;
-				case CallAudioState.ROUTE_EARPIECE:
-					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
-					break;
-				case CallAudioState.ROUTE_BLUETOOTH:
-					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.BLUETOOTH);
-					break;
-				default:
-					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.NONE);
-			}
+			rtpConnection.get().callIntegration.onCallAudioStateChanged(state);
 
 			try {
 				rtpConnection.get().setMicrophoneEnabled(!state.isMuted());

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

@@ -150,7 +150,7 @@ public class DownloadDefaultStickers extends Service {
 
 		NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
 		mBuilder.setContentTitle("Downloading Stickers")
-				.setSmallIcon(R.drawable.ic_archive_white_24dp)
+				.setSmallIcon(R.drawable.ic_archive_24dp)
 				.setProgress(1, 0, false);
 		startForeground(NOTIFICATION_ID, mBuilder.build());
 

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

@@ -2,7 +2,7 @@ package com.cheogram.android;
 
 import android.app.Activity;
 import android.content.Context;
-import android.graphics.PorterDuff;
+import android.content.res.ColorStateList;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -10,11 +10,14 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 
+import com.google.android.material.color.MaterialColors;
+
 import com.tokenautocomplete.TokenCompleteTextView;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.ListItem;
 import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.XEP0392Helper;
 
 public class TagEditorView extends TokenCompleteTextView<ListItem.Tag> {
 	public TagEditorView(Context context, AttributeSet attrs) {
@@ -36,13 +39,13 @@ public class TagEditorView extends TokenCompleteTextView<ListItem.Tag> {
 		LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
 		final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, (ViewGroup) getParent(), false);
 		tv.setText(tag.getName());
-		tv.getBackground().mutate().setColorFilter(tag.getColor(), PorterDuff.Mode.SRC_IN);
+      tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(tag.getName()))));
 		return tv;
 	}
 
 	@Override
 	protected ListItem.Tag defaultObject(String completionText) {
-		return new ListItem.Tag(completionText, UIHelper.getColorForName(completionText));
+		return new ListItem.Tag(completionText);
 	}
 
 	@Override

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

@@ -37,6 +37,7 @@ import androidx.core.graphics.drawable.IconCompat;
 import androidx.core.util.Consumer;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.color.MaterialColors;
 import com.google.common.io.ByteStreams;
 
 import io.ipfs.cid.Cid;
@@ -301,7 +302,7 @@ public class WebxdcPage implements ConversationPage {
 				TextView tv = (TextView) v.findViewById(android.R.id.text1);
 				tv.setGravity(Gravity.CENTER);
 				tv.setTextColor(ContextCompat.getColor(context, R.color.white));
-				tv.setBackgroundColor(UIHelper.getColorForName(getItem(position)));
+            tv.setBackgroundColor(MaterialColors.harmonizeWithPrimary(activity.get(),UIHelper.getColorForName(getItem(position))));
 				return v;
 			}
 		});

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

@@ -29,7 +29,6 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
 import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
 import eu.siacs.conversations.services.ImportBackupService;
 import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
-import eu.siacs.conversations.utils.ThemeHelper;
 
 public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
 
@@ -44,8 +43,6 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
-        this.mTheme = ThemeHelper.find(this);
-        setTheme(this.mTheme);
         super.onCreate(savedInstanceState);
         binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
         setSupportActionBar(binding.toolbar);
@@ -72,12 +69,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
     @Override
     public void onStart() {
         super.onStart();
-        final int theme = ThemeHelper.find(this);
-        if (this.mTheme != theme) {
-            recreate();
-        } else {
-            bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
-        }
+        bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
         final Intent intent = getIntent();
         if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) {
             Uri uri = intent.getData();

src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java 🔗

@@ -14,7 +14,7 @@ import java.security.SecureRandom;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.MagicCreateBinding;
+import eu.siacs.conversations.databinding.ActivityMagicCreateBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.InstallReferrerUtils;
@@ -26,7 +26,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
     public static final String EXTRA_PRE_AUTH = "pre_auth";
     public static final String EXTRA_USERNAME = "username";
 
-    private MagicCreateBinding binding;
+    private ActivityMagicCreateBinding binding;
     private String domain;
     private String username;
     private String preAuth;
@@ -37,19 +37,10 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
 
     }
 
-    @Override
-    public void onStart() {
-        super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
-    }
-
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
         final Intent data = getIntent();
@@ -60,7 +51,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         }
         super.onCreate(savedInstanceState);
-        this.binding = DataBindingUtil.setContentView(this, R.layout.magic_create);
+        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_magic_create);
         setSupportActionBar(this.binding.toolbar);
         configureActionBar(getSupportActionBar(), this.domain == null);
         if (username != null && domain != null) {

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

@@ -137,15 +137,6 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
         registerForContextMenu(accountListView);
     }
 
-    @Override
-    protected void onStart() {
-        super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
-    }
-
     @Override
     public void onSaveInstanceState(final Bundle savedInstanceState) {
         if (selectedAccount != null) {
@@ -173,7 +164,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         if (selectedAccountJid != null) {
             this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
         }

src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java 🔗

@@ -22,17 +22,17 @@ public class PickServerActivity extends XmppActivity {
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
 
     }
 
     @Override
     public void onStart() {
         super.onStart();
-        final int theme = findTheme();
+        /* final int theme = findTheme();
         if (this.mTheme != theme) {
             recreate();
-        }
+        }*/
     }
 
 

src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java 🔗

@@ -53,16 +53,7 @@ public class ShareViaAccountActivity extends XmppActivity {
     }
 
     @Override
-    protected void onStart() {
-        super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
-    }
-
-    @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         final int numAccounts = xmppConnectionService.getAccounts().size();
 
         if (numAccounts == 1) {

src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java 🔗

@@ -108,17 +108,13 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
 
     }
 
     @Override
     public void onStart() {
         super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
         new InstallReferrerUtils(this);
     }
 
@@ -159,7 +155,6 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
         binding.buttonPrivacy.setOnClickListener((v) ->
             startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://cheogram.com/android-privacy.html")))
         );
-        setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar(), false);
         binding.registerNewAccount.setOnClickListener(v -> {
             if (hasInviteUri()) {

src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java 🔗

@@ -22,7 +22,7 @@ import java.util.List;
 import java.util.concurrent.RejectedExecutionException;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.AccountRowBinding;
+import eu.siacs.conversations.databinding.ItemAccountBinding;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.ImportBackupService;
 import eu.siacs.conversations.utils.BackupFileHeader;
@@ -39,7 +39,7 @@ public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.Ba
     @NonNull
     @Override
     public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
-        return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false));
+        return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_account, viewGroup, false));
     }
 
     @Override
@@ -73,9 +73,9 @@ public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.Ba
     }
 
     static class BackupFileViewHolder extends RecyclerView.ViewHolder {
-        private final AccountRowBinding binding;
+        private final ItemAccountBinding binding;
 
-        BackupFileViewHolder(AccountRowBinding binding) {
+        BackupFileViewHolder(ItemAccountBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
         }

src/cheogram/res/drawable/background.xml → src/cheogram/res/drawable/background_splash_screen.xml 🔗

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <item android:drawable="@color/splash_screen_background" />
+    <item android:drawable="?colorPrimary" />
 
     <item
             android:gravity="center"

src/cheogram/res/drawable/business_black.xml 🔗

@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
-  <path
-      android:fillColor="@color/black54"
-      android:pathData="M12,7L12,3L2,3v18h20L22,7L12,7zM6,19L4,19v-2h2v2zM6,15L4,15v-2h2v2zM6,11L4,11L4,9h2v2zM6,7L4,7L4,5h2v2zM10,19L8,19v-2h2v2zM10,15L8,15v-2h2v2zM10,11L8,11L8,9h2v2zM10,7L8,7L8,5h2v2zM20,19h-8v-2h2v-2h-2v-2h2v-2h-2L12,9h8v10zM18,11h-2v2h2v-2zM18,15h-2v2h2v-2z"/>
-</vector>

src/cheogram/res/drawable/email_black.xml 🔗

@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
-  <path
-      android:fillColor="@color/black54"
-      android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
-</vector>

src/cheogram/res/drawable/email_white.xml 🔗

@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
-  <path
-      android:fillColor="@android:color/white"
-      android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
-</vector>

src/cheogram/res/drawable/business_white.xml → src/cheogram/res/drawable/ic_business_24dp.xml 🔗

@@ -3,7 +3,7 @@
     android:height="24dp"
     android:viewportWidth="24"
     android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
+    android:tint="?colorControlNormal">
   <path
       android:fillColor="@android:color/white"
       android:pathData="M12,7L12,3L2,3v18h20L22,7L12,7zM6,19L4,19v-2h2v2zM6,15L4,15v-2h2v2zM6,11L4,11L4,9h2v2zM6,7L4,7L4,5h2v2zM10,19L8,19v-2h2v2zM10,15L8,15v-2h2v2zM10,11L8,11L8,9h2v2zM10,7L8,7L8,5h2v2zM20,19h-8v-2h2v-2h-2v-2h2v-2h-2L12,9h8v10zM18,11h-2v2h2v-2zM18,15h-2v2h2v-2z"/>

src/cheogram/res/drawable/ic_email_24dp.xml 🔗

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?colorControlNormal"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z" />
+
+</vector>
@@ -3,7 +3,7 @@
     android:height="24dp"
     android:viewportWidth="24"
     android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
+    android:tint="?colorControlNormal">
   <path
       android:fillColor="@android:color/white"
       android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>

src/cheogram/res/drawable/ic_logo_mono.xml 🔗

@@ -0,0 +1,21 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="35dp"
+    android:height="32dp"
+    android:tint="?colorControlNormal"
+    android:viewportWidth="77.2339"
+    android:viewportHeight="75.11203">
+  <path
+      android:pathData="M20.1226,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="M35.5664,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
+      android:fillColor="#ffffff"/>
+  <path
+      android:pathData="M51.0105,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
+      android:fillColor="#ffffff"/>
+  <path

src/cheogram/res/drawable/ic_mood_24dp.xml 🔗

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M620,440Q645,440 662.5,422.5Q680,405 680,380Q680,355 662.5,337.5Q645,320 620,320Q595,320 577.5,337.5Q560,355 560,380Q560,405 577.5,422.5Q595,440 620,440ZM340,440Q365,440 382.5,422.5Q400,405 400,380Q400,355 382.5,337.5Q365,320 340,320Q315,320 297.5,337.5Q280,355 280,380Q280,405 297.5,422.5Q315,440 340,440ZM480,700Q548,700 603.5,661.5Q659,623 684,560L276,560Q301,623 356.5,661.5Q412,700 480,700ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800Z"/>
+</vector>

src/cheogram/res/drawable/irc.xml 🔗

@@ -2,10 +2,11 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="101.53829dp"
     android:height="50.769199dp"
+    android:tint="?colorControlNormal"
     android:viewportWidth="101.53829"
     android:viewportHeight="50.769199">
     <path
-        android:fillColor="?attr/edit_text_color"
+        android:fillColor="@android:color/white"
         android:strokeWidth="1"
@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
-  <path
-      android:fillColor="@color/black54"
-      android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
-</vector>

src/cheogram/res/drawable/list_choice.xml 🔗

@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:enterFadeDuration="@android:integer/config_shortAnimTime"
-    android:exitFadeDuration="@android:integer/config_shortAnimTime">
-
-    <item android:state_pressed="true" android:drawable="@color/grey500" />
-    <item android:state_activated="true" android:drawable="@color/grey500" />
-    <item android:drawable="@android:color/transparent" />
-</selector>

src/cheogram/res/drawable/matrix.xml 🔗

@@ -2,6 +2,7 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="27.9dp"
     android:height="32dp"
+    android:tint="?colorControlNormal"
     android:viewportWidth="27.9"
     android:viewportHeight="32">
 
@@ -9,13 +10,13 @@
             android:translateX="-.095"
             android:translateY=".005">
         <path
-            android:fillColor="?attr/edit_text_color"
+            android:fillColor="@android:color/white"
             android:pathData="M27.1 31.2v-30.5h-2.19v-0.732h3.04v32h-3.04v-0.732z" />
         <path
-            android:fillColor="?attr/edit_text_color"
+            android:fillColor="@android:color/white"
             android:pathData="M8.23 10.4v1.54h0.044c0.385-0.564 0.893-1.03 1.49-1.37 0.58-0.323 1.25-0.485 1.99-0.485 0.72 0 1.38 0.14 1.97 0.42 0.595 0.279 1.05 0.771 1.36 1.48 0.338-0.5 0.796-0.941 1.38-1.32 0.58-0.383 1.27-0.574 2.06-0.574 0.602 0 1.16 0.074 1.67 0.22 0.514 0.148 0.954 0.383 1.32 0.707 0.366 0.323 0.653 0.746 0.859 1.27 0.205 0.522 0.308 1.15 0.308 1.89v7.63h-3.13v-6.46c0-0.383-0.015-0.743-0.044-1.08-0.0209-0.307-0.103-0.607-0.242-0.882-0.133-0.251-0.336-0.458-0.584-0.596-0.257-0.146-0.606-0.22-1.05-0.22-0.44 0-0.796 0.085-1.07 0.253-0.272 0.17-0.485 0.39-0.639 0.662-0.159 0.287-0.264 0.602-0.308 0.927-0.052 0.347-0.078 0.697-0.078 1.05v6.35h-3.13v-6.4c0-0.338-0.007 -0.673-0.021-1-0.0114-0.314-0.0749-0.623-0.188-0.916-0.108-0.277-0.3-0.512-0.55-0.673-0.258-0.168-0.636-0.253-1.14-0.253-0.198 0.0083-0.394 0.042-0.584 0.1-0.258 0.0745-0.498 0.202-0.705 0.374-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.36v6.62h-3.13v-11.4z" />
         <path
-            android:fillColor="?attr/edit_text_color"
+            android:fillColor="@android:color/white"
             android:pathData="M0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z" />
     </group>
 </vector>

src/cheogram/res/drawable/palette_24px.xml 🔗

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"

src/cheogram/res/drawable/pill.xml 🔗

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
 	<corners android:radius="50dp" />
-	<solid android:color="?color_background_tertiary" />
+	<solid android:color="?colorTertiary" />
 </shape>

src/cheogram/res/drawable/subject.xml 🔗

@@ -1,10 +1,10 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="24dp"
     android:height="24dp"
+    android:tint="?colorControlNormal"
     android:viewportWidth="960"
-    android:viewportHeight="960"
-    android:autoMirrored="true">
+    android:viewportHeight="960">
   <path
-      android:fillColor="?icon_tint"
+      android:fillColor="@android:color/white"
       android:pathData="M160,760L160,680L560,680L560,760L160,760ZM160,600L160,520L800,520L800,600L160,600ZM160,440L160,360L800,360L800,440L160,440ZM160,280L160,200L800,200L800,280L160,280Z"/>
 </vector>

src/cheogram/res/drawable/textsend.xml 🔗

@@ -5,5 +5,5 @@
 			android:bottomRightRadius="0dp"
 			android:topLeftRadius="20dp"
 			android:topRightRadius="20dp" />
-	<solid android:color="?color_background_primary" />
+	<solid android:color="?colorSurface" />
 </shape>

src/cheogram/res/layout/actionview_edit.xml 🔗

@@ -8,7 +8,6 @@
 
     <EditText
         android:id="@+id/search_field"
-        style="@style/Widget.Conversations.SearchView"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:focusable="true"

src/cheogram/res/layout/activity_easy_invite.xml 🔗

@@ -1,17 +1,23 @@
 <?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">
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
 
     <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"
-        android:background="?attr/color_background_primary"
         android:orientation="vertical">
 
-        <include
-            android:id="@+id/toolbar"
-            layout="@layout/toolbar" />
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
 
         <LinearLayout
             android:id="@+id/in_progress"
@@ -41,7 +47,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:text="@string/tap_share_button_send_invite"
-                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                android:textAppearance="?textAppearanceBodyMedium" />
 
             <TextView
                 android:id="@+id/scan_the_code"
@@ -50,36 +56,31 @@
                 android:layout_below="@+id/tap_to_share"
                 android:layout_marginTop="24sp"
                 android:text="@string/if_contact_is_nearby_use_qr"
-                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                android:textAppearance="?textAppearanceBodyMedium" />
 
             <ImageView
                 android:id="@+id/qr_code"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
                 android:layout_above="@+id/share_button"
                 android:layout_below="@id/scan_the_code"
                 android:layout_alignParentStart="true"
-                android:layout_alignParentRight="true"
+                android:layout_alignParentEnd="true"
                 android:layout_centerHorizontal="true"
                 android:layout_margin="24sp"
                 android:scaleType="fitCenter" />
 
             <Button
                 android:id="@+id/share_button"
-                style="@style/Widget.Conversations.Button.Borderless"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_above="@+id/use_myself_button"
-                android:minWidth="0dp"
-                android:paddingLeft="16dp"
-                android:paddingRight="16dp"
-                android:text="@string/share"
+                android:layout_alignParentBottom="true"
                 android:layout_centerHorizontal="true"
-                android:textColor="?attr/colorAccent" />
+                android:layout_marginHorizontal="16dp"
+                android:text="@string/share" />
 
             <Button
                 android:id="@+id/use_myself_button"
-                style="@style/Widget.Conversations.Button.Borderless"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_alignParentBottom="true"

src/cheogram/res/layout/activity_import_backup.xml 🔗

@@ -2,22 +2,30 @@
 <layout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto">
 
-
     <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"
-        android:background="?attr/color_background_primary"
         android:orientation="vertical">
 
-        <include
-            android:id="@+id/toolbar"
-            layout="@layout/toolbar" />
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
+
         <LinearLayout
-            android:visibility="gone"
             android:id="@+id/in_progress"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:gravity="center">
+            android:gravity="center"
+            android:visibility="gone">
+
             <ProgressBar
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
@@ -25,18 +33,15 @@
         </LinearLayout>
 
 
-
         <androidx.coordinatorlayout.widget.CoordinatorLayout
             android:id="@+id/coordinator"
             android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="?attr/color_background_primary">
+            android:layout_height="match_parent">
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/list"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
-                android:background="?attr/color_background_primary"
                 android:orientation="vertical"
                 app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
         </androidx.coordinatorlayout.widget.CoordinatorLayout>

src/cheogram/res/layout/magic_create.xml → src/cheogram/res/layout/activity_magic_create.xml 🔗

@@ -7,7 +7,18 @@
         android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <include layout="@layout/toolbar" android:id="@+id/toolbar"/>
+        <com.google.android.material.appbar.AppBarLayout
+            android:id="@+id/app_bar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
 
         <ScrollView
             android:layout_width="match_parent"
@@ -16,15 +27,13 @@
 
             <RelativeLayout
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/color_background_primary">
+                android:layout_height="wrap_content">
 
                 <LinearLayout
                     android:id="@+id/linearLayout"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true"
                     android:layout_alignParentBottom="true"
                     android:minHeight="256dp"
                     android:orientation="vertical"
@@ -42,50 +51,53 @@
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:text="@string/pick_your_username"
-                        android:textAppearance="@style/TextAppearance.Conversations.Title" />
+                        android:textAppearance="?textAppearanceTitleLarge" />
 
                     <TextView
                         android:id="@+id/instructions"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_marginTop="8dp"
+                        android:layout_marginVertical="8dp"
                         android:text="@string/magic_create_text"
-                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                        android:textAppearance="?textAppearanceBodyMedium" />
 
-                    <EditText
-                        android:id="@+id/username"
+                    <com.google.android.material.textfield.TextInputLayout
+                        android:id="@+id/username_layout"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="center_horizontal"
-                        android:hint="@string/username_hint"
-                        android:textColor="?attr/edit_text_color"
-                        android:inputType="textNoSuggestions" />
+                        android:hint="@string/username_hint">
+
+                        <com.google.android.material.textfield.TextInputEditText
+                            android:id="@+id/username"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:inputType="textNoSuggestions" />
+                    </com.google.android.material.textfield.TextInputLayout>
 
                     <TextView
                         android:id="@+id/full_jid"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_marginTop="8dp"
+                        android:layout_marginVertical="8dp"
                         android:text="@string/your_full_jid_will_be"
-                        android:textAppearance="@style/TextAppearance.Conversations.Caption"
+                        android:textAppearance="?textAppearanceLabelSmall"
                         android:visibility="invisible" />
 
                     <Button
                         android:id="@+id/create_account"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TonalButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/next"
-                        android:textColor="?colorAccent" />
+                        android:layout_gravity="end"
+                        android:text="@string/next" />
                 </LinearLayout>
 
                 <RelativeLayout
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:layout_above="@+id/linearLayout"
-                    android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true">
+                    android:layout_alignParentStart="true">
 
                     <ImageView
                         android:layout_width="wrap_content"

src/cheogram/res/layout/activity_pick_server.xml 🔗

@@ -7,7 +7,17 @@
         android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <include android:id="@+id/toolbar" layout="@layout/toolbar" />
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
 
         <ScrollView
             android:layout_width="match_parent"
@@ -16,15 +26,13 @@
 
             <RelativeLayout
                 android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:background="?attr/color_background_primary">
+                android:layout_height="wrap_content">
 
                 <LinearLayout
                     android:id="@+id/linearLayout"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true"
                     android:layout_alignParentBottom="true"
                     android:minHeight="256dp"
                     android:orientation="vertical"
@@ -41,40 +49,38 @@
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:text="@string/pick_a_server"
-                        android:textAppearance="@style/TextAppearance.Conversations.Title" />
+                        android:textAppearance="?textAppearanceTitleLarge" />
 
                     <TextView
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:layout_marginTop="8dp"
+                        android:layout_marginBottom="16dp"
                         android:text="@string/server_select_text"
-                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                        android:textAppearance="?textAppearanceBodyMedium" />
 
                     <Button
                         android:id="@+id/use_cim"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TonalButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/use_conversations.im"
-                        android:textColor="?colorAccent" />
+                        android:layout_gravity="end"
+                        android:text="@string/use_conversations.im" />
 
                     <Button
                         android:id="@+id/use_own_provider"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TextButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/use_own_provider"
-                        android:textColor="?android:textColorSecondary" />
+                        android:layout_gravity="end"
+                        android:text="@string/use_own_provider" />
                 </LinearLayout>
 
                 <RelativeLayout
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:layout_above="@+id/linearLayout"
-                    android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true">
+                    android:layout_alignParentStart="true">
 
                     <ImageView
                         android:layout_width="wrap_content"

src/cheogram/res/layout/activity_welcome.xml 🔗

@@ -47,7 +47,6 @@
             android:layout_height="wrap_content"
             android:layout_marginBottom="15dp"
             android:fontFamily="@font/poppins"
-            android:textColor="?attr/edit_text_color"
             android:text="Cheogram Android is an app that connects you to a global network called Jabber.  This network includes services, called gateways, for chatting with other networks such as SMS, IRC, Matrix, and more." />
 
         <com.wefika.flowlayout.FlowLayout
@@ -75,14 +74,14 @@
                 android:layout_height="60dp"
                 android:layout_margin="10dp"
                 android:adjustViewBounds="true"
-                android:src="?attr/ic_make_audio_call" />
+                android:src="@drawable/ic_call_24dp" />
 
             <ImageView
                 android:layout_width="60dp"
                 android:layout_height="60dp"
                 android:layout_margin="10dp"
                 android:adjustViewBounds="true"
-                android:src="?attr/icon_email" />
+                android:src="@drawable/ic_email_48dp" />
 
             <ImageView
                 android:layout_width="60dp"
@@ -156,7 +155,6 @@
             android:layout_height="wrap_content"
             android:layout_marginBottom="15dp"
             android:fontFamily="@font/poppins"
-            android:textColor="?attr/edit_text_color"
             android:text="The Jabber network is powered by a protocol called XMPP. It is a decentralized network, which means no one owns or controls the whole thing. It is a federated network which means anyone can run a Jabber service. Messages to other Jabber users can be end-to-end encrypted by enabling OMEMO for the conversation.\n\nAddresses are called Jabber IDs and look similar to email addresses:" />
 
         <TextView
@@ -183,10 +181,6 @@
         android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <include
-            android:id="@+id/toolbar"
-            layout="@layout/toolbar" />
-
         <ScrollView
             android:layout_width="match_parent"
             android:layout_height="match_parent"
@@ -197,7 +191,7 @@
                 android:layout_height="wrap_content"
                 android:weightSum="2"
                 android:orientation="vertical"
-                android:background="?attr/color_background_primary">
+                android:background="?colorSurface">
 
                 <RelativeLayout
                     android:layout_width="match_parent"
@@ -226,59 +220,55 @@
                     android:paddingRight="16dp"
                     android:paddingBottom="10dp">
 
-                    <Button
+                    <com.google.android.material.button.MaterialButton
                         android:id="@+id/register_new_account"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TextButton"
                         android:textAllCaps="false"
-                        android:textSize="?TextSizeTitle"
+                        android:textSize="20sp"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:layout_marginBottom="10dp"
                         android:layout_gravity="center"
                         android:drawableLeft="@drawable/getjid"
                         android:fontFamily="@font/poppins"
-                        android:text="@string/create_new_account"
-                        android:textColor="?colorAccent" />
+                        android:text="@string/create_new_account" />
 
-                    <Button
+                    <com.google.android.material.button.MaterialButton
                         android:id="@+id/use_existing"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TextButton"
                         android:textAllCaps="false"
-                        android:textSize="?TextSizeTitle"
+                        android:textSize="20sp"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:layout_marginBottom="10dp"
                         android:layout_gravity="center"
                         android:drawableLeft="@drawable/havejid"
                         android:fontFamily="@font/poppins"
-                        android:text="@string/i_already_have_an_account"
-                        android:textColor="?colorAccent" />
+                        android:text="@string/i_already_have_an_account" />
 
-                    <Button
+                    <com.google.android.material.button.MaterialButton
                         android:id="@+id/use_snikket"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TextButton"
                         android:textAllCaps="false"
-                        android:textSize="?TextSizeTitle"
+                        android:textSize="20sp"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:layout_gravity="center"
                         android:drawableLeft="@drawable/snikket"
                         android:fontFamily="@font/poppins"
-                        android:text="@string/i_am_snikket_user"
-                        android:textColor="?colorAccent" />
+                        android:text="@string/i_am_snikket_user" />
 
-                    <Button
+                    <com.google.android.material.button.MaterialButton
                         android:id="@+id/use_backup"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TextButton"
                         android:textAllCaps="false"
-                        android:textSize="?TextSizeTitle"
+                        android:textSize="20sp"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:layout_gravity="center"
                         android:drawableLeft="@drawable/restore"
                         android:fontFamily="@font/poppins"
-                        android:text="@string/restore_backup"
-                        android:textColor="?colorAccent" />
+                        android:text="@string/restore_backup" />
                 </LinearLayout>
             </LinearLayout>
         </ScrollView>
@@ -314,7 +304,7 @@
             app:layout_constraintTop_toTopOf="parent"
             app:selectedDotColor="#FFFFFF" />
 
-        <Button
+        <com.google.android.material.button.MaterialButton
             android:id="@+id/button_next"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
@@ -330,7 +320,7 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toBottomOf="@id/dots_indicator" />
 
-        <Button
+        <com.google.android.material.button.MaterialButton
             android:id="@+id/button_privacy"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"

src/cheogram/res/layout/command_button.xml 🔗

@@ -1,12 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android">
-<TextView
+<com.google.android.material.button.MaterialButton
     android:id="@+id/command"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    style="?android:attr/buttonStyleSmall"
-    android:background="@drawable/pill"
-    android:paddingLeft="10dp"
-    android:paddingRight="10dp"
-    />
+    style="@style/Widget.Material3.Button.ElevatedButton" />
 </layout>

src/cheogram/res/layout/command_button_grid_field.xml 🔗

@@ -15,8 +15,7 @@
             android:paddingRight="8dp"
             android:paddingBottom="8dp"
             android:gravity="center"
-            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
         <TextView
             android:id="@+id/desc"
@@ -25,8 +24,7 @@
             android:paddingLeft="16dp"
             android:paddingRight="8dp"
             android:gravity="center"
-            android:textAppearance="@style/TextAppearance.Conversations.Status"
-            android:textColor="?android:textColorSecondary" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
         <Button
             android:id="@+id/default_button"
@@ -60,8 +58,7 @@
             android:layout_marginLeft="8dp"
             android:layout_marginTop="40dp"
             android:gravity="center"
-            android:text="other / custom"
-            style="@style/Widget.Conversations.Button.Borderless" />
+            android:text="other / custom" />
 
     </LinearLayout>
 </layout>

src/cheogram/res/layout/command_checkbox_field.xml 🔗

@@ -24,14 +24,14 @@
                 android:layout_height="wrap_content"
                 android:paddingLeft="13dp"
                 android:scrollHorizontally="false"
-                android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
+                android:textAppearance="?textAppearanceBodyMedium" />
 
             <TextView
                 android:id="@+id/desc"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:paddingLeft="16dp"
-                android:textAppearance="@style/TextAppearance.Conversations.Status"
+                android:textAppearance="?textAppearanceBodySmall"
                 android:textColor="?android:textColorSecondary" />
 
         </LinearLayout>

src/cheogram/res/layout/command_note.xml 🔗

@@ -13,8 +13,9 @@
             android:layout_width="match_parent"
             android:layout_height="50dp"
             android:layout_centerHorizontal="true"
-            android:scaleType="fitCenter"
-            android:src="@drawable/ic_send_cancel_dnd" />
+            android:src="@drawable/ic_cancel_96dp"
+            android:tint="?colorError"
+            android:scaleType="fitCenter" />
 
         <TextView
             android:id="@+id/message"
@@ -24,8 +25,7 @@
             android:minHeight="?android:attr/listPreferredItemHeightSmall"
             android:paddingLeft="8dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Body1"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
     </LinearLayout>
 </layout>

src/cheogram/res/layout/command_progress_bar.xml 🔗

@@ -16,7 +16,6 @@
             android:paddingRight="8dp"
             android:paddingBottom="16dp" />
 
-
         <TextView
             android:id="@+id/text"
             android:layout_width="match_parent"
@@ -25,9 +24,8 @@
             android:minHeight="?android:attr/listPreferredItemHeightSmall"
             android:paddingLeft="8dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Body1"
-            android:text="Please be patient..."
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium"
+            android:text="Please be patient..." />
 
     </LinearLayout>
 

src/cheogram/res/layout/command_radio_edit_field.xml 🔗

@@ -14,8 +14,7 @@
             android:paddingLeft="13dp"
             android:paddingRight="8dp"
             android:paddingBottom="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
         <com.cheogram.android.GridView
             android:id="@+id/radios"
@@ -30,7 +29,6 @@
         <EditText
             android:id="@+id/open"
             android:visibility="gone"
-            style="@style/Widget.Conversations.EditText"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginLeft="8dp"
@@ -46,8 +44,8 @@
             android:layout_height="wrap_content"
             android:paddingLeft="16dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Status"
-            android:textColor="?android:textColorSecondary" />
+            android:textStyle="italic"
+            android:textAppearance="?textAppearanceBodySmall" />
 
     </LinearLayout>
 </layout>

src/cheogram/res/layout/command_result_cell.xml 🔗

@@ -10,7 +10,6 @@
         android:paddingLeft="8dp"
         android:paddingRight="8dp"
         android:paddingBottom="8dp"
-        android:textAppearance="@style/TextAppearance.Conversations.Body1"
-        android:textColor="?attr/edit_text_color" />
+        android:textAppearance="?textAppearanceBodyMedium" />
 
 </layout>

src/cheogram/res/layout/command_result_field.xml 🔗

@@ -14,7 +14,7 @@
             android:paddingLeft="8dp"
             android:paddingRight="8dp"
             android:paddingBottom="4dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Caption" />
+            android:textAppearance="?textAppearanceLabelMedium" />
 
         <ImageView
             android:id="@+id/media_image"
@@ -36,7 +36,7 @@
             android:layout_alignParentStart="true"
             android:layout_alignParentLeft="true"
             android:layout_alignParentTop="true"
-            android:background="?attr/color_background_tertiary"
+            android:background="?colorSurfaceContainer"
             android:divider="@android:color/transparent"
             android:dividerHeight="0dp"></ListView>
 
@@ -46,8 +46,7 @@
             android:layout_height="wrap_content"
             android:paddingLeft="8dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Status"
-            android:textColor="?android:textColorSecondary" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
     </LinearLayout>
 </layout>

src/cheogram/res/layout/command_search_list_field.xml 🔗

@@ -14,12 +14,10 @@
             android:paddingLeft="13dp"
             android:paddingRight="8dp"
             android:paddingBottom="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
         <EditText
             android:id="@+id/search"
-            style="@style/Widget.Conversations.EditText"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginLeft="8dp"
@@ -40,8 +38,8 @@
             android:layout_height="wrap_content"
             android:paddingLeft="16dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Status"
-            android:textColor="?android:textColorSecondary" />
+            android:textStyle="italic"
+            android:textAppearance="?textAppearanceBodySmall" />
 
     </LinearLayout>
 </layout>

src/cheogram/res/layout/command_slider_field.xml 🔗

@@ -15,8 +15,7 @@
             android:paddingLeft="13dp"
             android:paddingRight="8dp"
             android:paddingBottom="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
         <com.google.android.material.slider.Slider
             android:id="@+id/slider"
@@ -24,9 +23,7 @@
             android:layout_height="wrap_content"
             android:paddingLeft="8dp"
             android:paddingRight="8dp"
-            android:paddingBottom="8dp"
-            app:trackColorInactive="?color_background_primary"
-            app:trackColorActive="?colorAccent" />
+            android:paddingBottom="8dp" />
 
         <TextView
             android:id="@+id/desc"
@@ -34,8 +31,8 @@
             android:layout_height="wrap_content"
             android:paddingLeft="16dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Status"
-            android:textColor="?android:textColorSecondary" />
+            android:textStyle="italic"
+            android:textAppearance="?textAppearanceBodySmall" />
 
     </LinearLayout>
 

src/cheogram/res/layout/command_spinner_field.xml 🔗

@@ -14,8 +14,7 @@
             android:paddingLeft="13dp"
             android:paddingRight="8dp"
             android:paddingBottom="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
         <Spinner
             android:id="@+id/spinner"
@@ -31,8 +30,8 @@
             android:layout_height="wrap_content"
             android:paddingLeft="16dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Status"
-            android:textColor="?android:textColorSecondary" />
+            android:textStyle="italic"
+            android:textAppearance="?textAppearanceBodySmall" />
 
     </LinearLayout>
 </layout>

src/cheogram/res/layout/command_text_field.xml 🔗

@@ -9,15 +9,10 @@
             android:paddingLeft="8dp"
             android:paddingRight="8dp"
             android:paddingBottom="16dp"
-            app:suffixTextAppearance="@style/Widget.Conversations.EditText"
-            app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"
-            app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
-            app:helperTextTextAppearance="@style/TextAppearance.Conversations.Status"
             app:helperTextTextColor="?android:textColorSecondary">
 
             <com.google.android.material.textfield.TextInputEditText
                 android:id="@+id/textinput"
-                style="@style/Widget.Conversations.EditText"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:ems="10"

src/cheogram/res/layout/command_webview.xml 🔗

@@ -14,8 +14,7 @@
             android:minHeight="?android:attr/listPreferredItemHeightSmall"
             android:paddingLeft="8dp"
             android:paddingRight="8dp"
-            android:textAppearance="@style/TextAppearance.Conversations.Body1"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
         <WebView
             android:id="@+id/webview"

src/cheogram/res/layout/dialog_enter_password.xml 🔗

@@ -1,47 +1,53 @@
 <?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:app="http://schemas.android.com/apk/res-auto">
 
-    <LinearLayout
+    <ScrollView
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:padding="?dialogPreferredPadding">
+        android:layout_height="match_parent">
 
-        <TextView
-            android:id="@+id/explain"
+        <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/enter_password_to_restore"
-            android:textAppearance="@style/TextAppearance.Conversations.Body2"/>
+            android:orientation="vertical"
+            android:padding="?dialogPreferredPadding">
+            
+            <TextView
+                android:id="@+id/explain"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/enter_password_to_restore"
+                android:textAppearance="?textAppearanceBodyMedium" />
 
-        <TextView
-            android:layout_marginTop="?TextSizeBody1"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/restore_warning"
-            android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="18sp"
+                android:text="@string/restore_warning"
+                android:textAppearance="?textAppearanceBodyMedium" />
 
-        <com.google.android.material.textfield.TextInputLayout
-            android:id="@+id/account_password_layout"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="8dp"
-            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">
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="18sp"
+                android:text="@string/restore_warning_continued"
+                android:textAppearance="?textAppearanceBodyMedium" />
 
-        <eu.siacs.conversations.ui.widget.TextInputEditText
-            android:id="@+id/account_password"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:hint="@string/password"
-            android:inputType="textPassword"
-            android:textColor="?attr/edit_text_color"
-            style="@style/Widget.Conversations.EditText"/>
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/account_password_layout"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                app:endIconMode="password_toggle">
+
+                <eu.siacs.conversations.ui.widget.TextInputEditText
+                    android:id="@+id/account_password"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="@string/password"
+                    android:inputType="textPassword" />
 
-        </com.google.android.material.textfield.TextInputLayout>
-    </LinearLayout>
+            </com.google.android.material.textfield.TextInputLayout>
+        </LinearLayout>
+    </ScrollView>
 </layout>

src/cheogram/res/layout/emoji_search.xml 🔗

@@ -9,7 +9,6 @@
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_alignParentTop="true"
-      android:background="?attr/color_background_secondary"
       android:divider="@android:color/transparent"
       android:dividerHeight="0dp"></ListView>
 

src/cheogram/res/layout/emoji_search_row.xml 🔗

@@ -10,26 +10,23 @@
 		android:paddingTop="8dp"
 		android:paddingBottom="8dp"
 		android:paddingLeft="@dimen/avatar_item_distance"
-		android:paddingRight="@dimen/avatar_item_distance"
-		android:background="@drawable/list_choice">
+		android:paddingRight="@dimen/avatar_item_distance">
 
 		<TextView
 				android:id="@+id/nonunicode"
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
 				android:gravity="center_vertical"
-				android:textAppearance="@style/TextAppearance.Conversations.Body1"
+				android:textAppearance="?textAppearanceBodyMedium"
 				app:emojiCompatEnabled="false"
-				android:visibility="gone"
-				android:textColor="?attr/edit_text_color" />
+				android:visibility="gone" />
 
 		<TextView
 				android:id="@+id/unicode"
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
 				android:gravity="center_vertical"
-				android:textAppearance="@style/TextAppearance.Conversations.Body1"
-				android:textColor="?attr/edit_text_color" />
+				android:textAppearance="?textAppearanceBodyMedium" />
 
 		<TextView
 				android:id="@+id/shortcode"
@@ -37,8 +34,7 @@
 				android:layout_height="wrap_content"
 				android:gravity="center_vertical"
 				android:paddingLeft="8dp"
-				android:textAppearance="@style/TextAppearance.Conversations.Body1"
-				android:textColor="?attr/edit_text_color" />
+				android:textAppearance="?textAppearanceBodyMedium" />
 
 	</LinearLayout>
 

src/cheogram/res/layout/thread_row.xml 🔗

@@ -8,7 +8,6 @@
         android:minHeight="?android:attr/listPreferredItemHeightSmall"
         android:paddingLeft="@dimen/avatar_item_distance"
         android:paddingRight="@dimen/avatar_item_distance"
-        android:background="@drawable/list_choice"
         android:orientation="horizontal">
 
         <com.lelloman.identicon.view.GithubIdenticonView
@@ -25,8 +24,7 @@
             android:gravity="center_vertical"
             android:maxLines="1"
             android:minHeight="?android:attr/listPreferredItemHeightSmall"
-            android:textAppearance="@style/TextAppearance.Conversations.Body1"
-            android:textColor="?attr/edit_text_color" />
+            android:textAppearance="?textAppearanceBodyMedium" />
 
     </LinearLayout>
 

src/cheogram/res/menu/conversations.xml 🔗

@@ -18,7 +18,6 @@
         app:showAsAction="never" />
     <item
         android:id="@+id/action_ongoing_call"
-        android:icon="?attr/icon_ongoing_call"
         android:orderInCategory="30"
         android:title="@string/return_to_ongoing_call"
         app:showAsAction="always" />
@@ -29,13 +28,12 @@
         app:showAsAction="never" />
     <item
         android:id="@+id/action_muc_details"
-        android:icon="?attr/icon_group"
         android:orderInCategory="40"
         android:title="@string/action_muc_details"
         app:showAsAction="never" />
     <item
         android:id="@+id/action_archive"
         android:orderInCategory="60"
-        android:title="@string/action_end_conversation"
+        android:title="@string/action_archive_chat"
         app:showAsAction="never" />
 </menu>

src/cheogram/res/menu/manageaccounts.xml 🔗

@@ -1,32 +1,31 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-	  xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
-	<item
-		android:id="@+id/action_add_account"
-		android:icon="?attr/icon_add_person"
-		app:showAsAction="always"
-		android:title="@string/action_add_account"/>
-	<item
-		android:id="@+id/action_import_backup"
-		app:showAsAction="never"
-		android:title="@string/restore_backup"/>
-	<item
-		android:id="@+id/action_add_account_with_cert"
-		app:showAsAction="never"
-		android:icon="?attr/icon_add_person"
-		android:title="@string/action_add_account_with_certificate"
-		android:visible="true"/>
-	<item
-		android:id="@+id/action_enable_all"
-		android:title="@string/enable_all_accounts"/>
-	<item
-		android:id="@+id/action_disable_all"
-		android:title="@string/disable_all_accounts"/>
-	<item
-		android:id="@+id/action_settings"
-		android:orderInCategory="100"
-		app:showAsAction="never"
-		android:title="@string/action_settings"/>
+    <item
+        android:id="@+id/action_add_account"
+        android:icon="@drawable/ic_person_add_24dp"
+        android:title="@string/action_add_account"
+        app:showAsAction="ifRoom" />
+    <item
+        android:id="@+id/action_import_backup"
+        android:title="@string/restore_backup"
+        app:showAsAction="never" />
+    <item
+        android:id="@+id/action_add_account_with_cert"
+        android:title="@string/action_add_account_with_certificate"
+        android:visible="true"
+        app:showAsAction="never" />
+    <item
+        android:id="@+id/action_enable_all"
+        android:title="@string/enable_all_accounts" />
+    <item
+        android:id="@+id/action_disable_all"
+        android:title="@string/disable_all_accounts" />
+    <item
+        android:id="@+id/action_settings"
+        android:orderInCategory="100"
+        android:title="@string/action_settings"
+        app:showAsAction="never" />
 
 </menu>

src/cheogram/res/menu/welcome_menu.xml 🔗

@@ -3,7 +3,7 @@
 
     <item
         android:id="@+id/action_scan_qr_code"
-        android:icon="?attr/icon_scan_qr_code"
+        android:icon="@drawable/ic_qr_code_scanner_24dp"
         android:orderInCategory="10"
         android:title="@string/scan_qr_code"
         android:visible="@bool/show_qr_code_scan"

src/cheogram/res/values/colors-themed.xml 🔗

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="md_theme_light_primary">#8224dd</color>
+    <color name="md_theme_light_onPrimary">#FFFFFF</color>
+    <color name="md_theme_light_primaryContainer">#efdbff</color>
+    <color name="md_theme_light_onPrimaryContainer">#2b0052</color>
+    <color name="md_theme_light_secondary">#774b9b</color>
+    <color name="md_theme_light_onSecondary">#FFFFFF</color>
+    <color name="md_theme_light_secondaryContainer">#f2daff</color>
+    <color name="md_theme_light_onSecondaryContainer">#2e004e</color>
+    <color name="md_theme_light_tertiary">#765b00</color>
+    <color name="md_theme_light_onTertiary">#FFFFFF</color>
+    <color name="md_theme_light_tertiaryContainer">#ffdf94</color>
+    <color name="md_theme_light_onTertiaryContainer">#251a00</color>
+    <color name="md_theme_light_error">#BA1A1A</color>
+    <color name="md_theme_light_errorContainer">#FFDAD6</color>
+    <color name="md_theme_light_onError">#FFFFFF</color>
+    <color name="md_theme_light_onErrorContainer">#410002</color>
+    <color name="md_theme_light_background">#fffbff</color>
+    <color name="md_theme_light_onBackground">#1d1b1e</color>
+    <color name="md_theme_light_surface">#fffbff</color>
+    <color name="md_theme_light_onSurface">#1d1b1e</color>
+    <color name="md_theme_light_surfaceVariant">#e9e0eb</color>
+    <color name="md_theme_light_onSurfaceVariant">#4a454e</color>
+    <color name="md_theme_light_outline">#7b757f</color>
+    <color name="md_theme_light_inverseOnSurface">#F0F1EB</color>
+    <color name="md_theme_light_inverseSurface">#2F312D</color>
+    <color name="md_theme_light_inversePrimary">#7DDC7A</color>
+    <color name="md_theme_light_shadow">#000000</color>
+    <color name="md_theme_light_surfaceTint">#006E1C</color>
+    <color name="md_theme_light_outlineVariant">#C2C9BD</color>
+    <color name="md_theme_light_scrim">#000000</color>
+    <color name="md_theme_dark_primary">#dbb8ff</color>
+    <color name="md_theme_dark_onPrimary">#470083</color>
+    <color name="md_theme_dark_primaryContainer">#6600b7</color>
+    <color name="md_theme_dark_onPrimaryContainer">#efdbff</color>
+    <color name="md_theme_dark_secondary">#e0b6ff</color>
+    <color name="md_theme_dark_onSecondary">#461969</color>
+    <color name="md_theme_dark_secondaryContainer">#5e3281</color>
+    <color name="md_theme_dark_onSecondaryContainer">#f2daff</color>
+    <color name="md_theme_dark_tertiary">#f5bf00</color>
+    <color name="md_theme_dark_onTertiary">#3e2e00</color>
+    <color name="md_theme_dark_tertiaryContainer">#594400</color>
+    <color name="md_theme_dark_onTertiaryContainer">#ffdf94</color>
+    <color name="md_theme_dark_error">#FFB4AB</color>
+    <color name="md_theme_dark_errorContainer">#93000A</color>
+    <color name="md_theme_dark_onError">#690005</color>
+    <color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
+    <color name="md_theme_dark_background">#1d1b1e</color>
+    <color name="md_theme_dark_onBackground">#e7e1e5</color>
+    <color name="md_theme_dark_surface">#1d1b1e</color>
+    <color name="md_theme_dark_onSurface">#e7e1e5</color>
+    <color name="md_theme_dark_surfaceVariant">#4a454e</color>
+    <color name="md_theme_dark_onSurfaceVariant">#ccc4cf</color>
+    <color name="md_theme_dark_outline">#958e98</color>
+    <color name="md_theme_dark_inverseOnSurface">#1A1C19</color>
+    <color name="md_theme_dark_inverseSurface">#E2E3DD</color>
+    <color name="md_theme_dark_inversePrimary">#006E1C</color>
+    <color name="md_theme_dark_shadow">#000000</color>
+    <color name="md_theme_dark_surfaceTint">#7DDC7A</color>
+    <color name="md_theme_dark_outlineVariant">#424940</color>
+    <color name="md_theme_dark_scrim">#000000</color>
+</resources>

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

@@ -6,19 +6,7 @@
 	<color name="perpy_desaturated_light">#8534e0</color>
 	<color name="perpy_desaturated">#6c29b8</color>
 	<color name="black_perpy">#1E0036</color>
+	<color name="blacker_perpy">#0E0020</color>
 	<color name="yeller">#FFC700</color>
-
-	<color name="custom_theme_primary">@color/perpy</color>
-	<color name="custom_theme_primary_dark">@color/black_perpy</color>
-	<color name="custom_theme_accent">@color/black_perpy</color>
-	<color name="custom_theme_background_primary">@color/grey50</color>
-	<color name="custom_theme_background_secondary">@color/grey200</color>
-	<color name="custom_theme_background_tertiary">@color/grey300</color>
-
-	<color name="custom_dark_theme_primary">@color/perpy</color>
-	<color name="custom_dark_theme_primary_dark">@color/black_perpy</color>
-	<color name="custom_dark_theme_accent">@color/black_perpy</color>
-	<color name="custom_dark_theme_background_primary">@color/grey800</color>
-	<color name="custom_dark_theme_background_secondary">@color/grey900</color>
-	<color name="custom_dark_theme_background_tertiary">@color/grey700</color>
+	<color name="white">#FFFFFF</color>
 </resources>

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

@@ -18,4 +18,14 @@
 	<item name="cornerSize">@dimen/incoming_call_radius</item>
 </style>
 
+    <style name="DialpadNumberStyle">
+        <item name="android:includeFontPadding">false</item>
+        <item name="android:textSize">@dimen/dialpad_text_size</item>
+    </style>
+
+    <style name="DialpadLetterStyle">
+        <item name="android:textSize">@dimen/smaller_text_size</item>
+        <item name="android:alpha">0.8</item>
+    </style>
+
 </resources>

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

@@ -1,585 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:tools="http://schemas.android.com/tools">
-
-    <style name="ConversationsTheme" parent="Theme.AppCompat.Light.NoActionBar">
-        <item name="colorPrimary">@color/perpy</item>
-        <item name="colorPrimaryDark">@color/black_perpy</item>
-        <item name="colorAccent">@color/black_perpy</item>
-        <item name="popupOverlayStyle">@style/ThemeOverlay.AppCompat.Light</item>
-
-        <item name="colorOnPrimary">?edit_text_color</item>
-        <item name="colorOnBackground">?colorPrimaryDark</item>
-        <item name="colorSurface">?colorPrimaryDark</item>
-
-        <item name="message_bubble_received_bg">@color/perpy_desaturated_light</item>
-        <item name="message_bubble_sent_bg">?color_background_primary</item>
-        <item name="message_bubble_shadow_light">#00CCCCCC</item>
-        <item name="message_bubble_shadow_dark">#FFCCCCCC</item>
-        <item name="message_bubble_shadow_dark_top">#DDCCCCCC</item>
-
-        <item name="color_background_primary">@color/grey50</item>
-        <item name="color_background_secondary">@color/grey200</item>
-        <item name="color_background_tertiary">@color/grey300</item>
-        <item name="color_background_overlay">@color/black12</item>
-        <item name="color_warning">@color/red_a700</item>
-        <item name="TextColorOnline">@color/green600</item>
-        <item name="TextColorError">@color/red800</item>
-        <item name="edit_text_color">@color/black87</item>
-        <item name="icon_tint">@color/black54</item>
-
-        <item name="activity_background_search">@drawable/search_background_light</item>
-        <item name="activity_background_no_results">@drawable/no_results_background_light</item>
-        <item name="activity_primary_background_no_results">
-            @drawable/no_results_primary_background_light
-        </item>
-        <item name="list_item_background">@drawable/list_item_background_light</item>
-
-        <item name="EmojiColor">@color/black</item>
-
-        <item name="windowActionModeOverlay">true</item>
-        <item name="android:actionModeBackground">?colorPrimary</item>
-
-        <item name="TextSizeCaption">12sp</item>
-        <item name="TextSizeBody1">14sp</item>
-        <item name="TextSizeBody2">14sp</item>
-        <item name="TextSizeSubhead">16sp</item>
-        <item name="TextSizeTitle">20sp</item>
-        <item name="TextSizeDisplay2">45sp</item>
-        <item name="TextSizeInput">16sp</item>
-        <item name="TextSeparation">5sp</item>
-        <item name="IconSize">18sp</item>
-
-        <item name="divider">@color/black12</item>
-
-        <item name="ic_send_cancel_offline" type="reference">@drawable/ic_send_cancel_offline</item>
-        <item name="ic_send_location_offline" type="reference">@drawable/ic_send_location_offline
-        </item>
-        <item name="ic_send_photo_offline" type="reference">@drawable/ic_send_photo_offline</item>
-        <item name="ic_send_picture_offline" type="reference">@drawable/ic_send_picture_offline
-        </item>
-        <item name="ic_send_text_offline" type="reference">@drawable/ic_send_text_offline</item>
-        <item name="ic_send_videocam_offline" type="reference">@drawable/ic_send_videocam_offline
-        </item>
-        <item name="ic_send_voice_offline" type="reference">@drawable/ic_send_voice_offline</item>
-
-        <item name="ic_attach_camera" type="reference">@drawable/ic_attach_camera</item>
-        <item name="ic_attach_videocam" type="reference">@drawable/ic_attach_videocam</item>
-        <item name="ic_attach_document" type="reference">@drawable/ic_attach_document</item>
-        <item name="ic_attach_location" type="reference">@drawable/ic_attach_location</item>
-        <item name="ic_attach_photo" type="reference">@drawable/ic_attach_photo</item>
-        <item name="ic_attach_record" type="reference">@drawable/ic_attach_record</item>
-
-        <item name="ic_make_audio_call" type="reference">@drawable/ic_call_black54_24dp</item>
-        <item name="ic_make_video_call" type="reference">@drawable/ic_videocam_black54_24dp</item>
-
-        <item name="message_bubble_sent" type="reference">@drawable/message_bubble_sent</item>
-        <item name="message_bubble_received" type="reference">
-            @drawable/message_bubble_received
-        </item>
-
-        <item name="unread_count">?colorPrimary</item>
-
-        <item name="conversations_overview_background">?colorPrimary</item>
-
-        <item name="icon_alpha" type="float">0.54</item>
-        <item name="delete_icon_alpha" type="float">0.70</item>
-
-        <item name="dialog_horizontal_padding">24dp</item>
-        <item name="dialog_vertical_padding">16dp</item>
-
-        <item name="media_preview_image" type="reference">@drawable/ic_image_black_48dp</item>
-        <item name="media_preview_document" type="reference">@drawable/ic_description_black_48dp</item>
-        <item name="media_preview_recording" type="reference">@drawable/ic_mic_black_48dp</item>
-        <item name="media_preview_audio" type="reference">@drawable/ic_headset_black_48dp</item>
-        <item name="media_preview_location" type="reference">@drawable/ic_room_black_48dp</item>
-        <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_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>
-        <item name="media_preview_backup" type="reference">@drawable/ic_backup_black_48dp</item>
-        <item name="media_preview_unknown" type="reference">@drawable/ic_help_black_48dp</item>
-
-        <item name="icon_link" type="reference">@drawable/link_black</item>
-        <item name="icon_email" type="reference">@drawable/email_black</item>
-        <item name="icon_org" type="reference">@drawable/business_black</item>
-        <item name="icon_chat" type="reference">@drawable/ic_chat_black_24dp</item>
-        <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>
-        <item name="icon_edit_body" type="reference">@drawable/ic_edit_black_24dp</item>
-        <item name="icon_save" type="reference">@drawable/ic_save_black_24dp</item>
-        <item name="icon_group" type="reference">@drawable/ic_group_white_24dp</item>
-        <item name="icon_new" type="reference">@drawable/ic_add_white_24dp</item>
-        <item name="icon_quote" type="reference">@drawable/ic_reply_black</item>
-        <item name="icon_refresh" type="reference">@drawable/ic_refresh_black_24dp</item>
-        <item name="icon_new_attachment" type="reference">@drawable/ic_attach_file_white_24dp</item>
-        <item name="icon_not_secure" type="reference">@drawable/ic_lock_open_white_24dp</item>
-        <item name="icon_call" type="reference">@drawable/ic_call_white_24dp</item>
-        <item name="icon_ongoing_call" type="reference">@drawable/ic_phone_in_talk_white_24dp</item>
-        <item name="ic_ongoing_call_hint" type="reference">@drawable/ic_phone_in_talk_black_18dp
-        </item>
-        <item name="icon_remove" type="reference">@drawable/ic_delete_black_24dp</item>
-        <item name="icon_search" type="reference">@drawable/ic_search_white_24dp</item>
-        <item name="icon_help" type="reference">@drawable/ic_help_white_24dp</item>
-        <item name="icon_goto_chat" type="reference">@drawable/ic_question_answer_white_24dp</item>
-        <item name="icon_secure" type="reference">@drawable/ic_lock_open_white_24dp</item>
-        <item name="icon_small_lock" type="reference">@drawable/ic_lock_black_18dp</item>
-        <item name="icon_settings" type="reference">@drawable/ic_settings_black_24dp</item>
-        <item name="icon_share" type="reference">@drawable/ic_share_white_24dp</item>
-        <item name="ic_cloud_download" type="reference">@drawable/ic_cloud_download_white_24dp
-        </item>
-        <item name="icon_scan_qr_code" type="reference">@drawable/ic_qr_code_scan_white_24dp</item>
-        <item name="icon_scroll_down" type="reference">@drawable/ic_scroll_to_end_black</item>
-
-        <item name="icon_gps_not_fixed" type="reference">@drawable/ic_gps_not_fixed_black_24dp
-        </item>
-        <item name="icon_gps_fixed" type="reference">@drawable/ic_gps_fixed_black_24dp</item>
-        <item name="icon_directions" type="reference">@drawable/ic_directions_black_24dp</item>
-        <item name="icon_copy_bar" type="reference">@drawable/ic_content_copy_white_24dp</item>
-
-        <item name="icon_notifications" type="reference">@drawable/ic_notifications_black_24dp
-        </item>
-        <item name="icon_notifications_off" type="reference">
-            @drawable/ic_notifications_off_black_24dp
-        </item>
-        <item name="icon_notifications_paused" type="reference">
-            @drawable/ic_notifications_paused_black_24dp
-        </item>
-        <item name="icon_notifications_none" type="reference">
-            @drawable/ic_notifications_none_black_24dp
-        </item>
-        <item name="icon_pinned_on_top" type="reference">@drawable/ic_star_black_24dp</item>
-        <item name="icon_enable_undecided_device" type="reference">
-            @drawable/ic_new_releases_black_24dp
-        </item>
-    </style>
-
-    <style name="ConversationsTheme.Dark" parent="Theme.AppCompat.NoActionBar">
-        <item name="colorPrimary">@color/perpy</item>
-        <item name="colorPrimaryDark">@color/black_perpy</item>
-        <item name="colorAccent">@color/yeller</item>
-        <item name="popupOverlayStyle">@style/ThemeOverlay.AppCompat.Dark</item>
-        <item name="android:navigationBarColor" tools:targetApi="21">@color/black</item>
-
-        <item name="message_bubble_received_bg">@color/perpy_desaturated</item>
-        <item name="message_bubble_sent_bg">?color_background_primary</item>
-        <item name="message_bubble_shadow_light">#00424242</item>
-        <item name="message_bubble_shadow_dark">#FF424242</item>
-        <item name="message_bubble_shadow_dark_top">#DD424242</item>
-
-        <item name="color_background_primary">@color/grey800</item>
-        <item name="color_background_secondary">@color/grey900</item>
-        <item name="color_background_tertiary">@color/grey700</item>
-        <item name="color_background_overlay">@color/black26</item>
-        <item name="activity_background_search">@drawable/search_background_dark</item>
-        <item name="activity_background_no_results">@drawable/no_results_background_dark</item>
-        <item name="activity_primary_background_no_results">
-            @drawable/no_results_primary_background_dark
-        </item>
-        <item name="list_item_background">@drawable/list_item_background_dark</item>
-        <item name="color_warning">@color/red_a100</item>
-
-        <item name="TextColorOnline">@color/green500</item>
-        <item name="TextColorError">@color/red500</item>
-        <item name="edit_text_color">@color/white</item>
-        <item name="icon_tint">@color/white70</item>
-
-        <item name="EmojiColor">@color/white</item>
-
-        <item name="windowActionModeOverlay">true</item>
-        <item name="android:actionModeBackground">?colorPrimary</item>
-
-        <item name="TextSizeCaption">12sp</item>
-        <item name="TextSizeBody1">14sp</item>
-        <item name="TextSizeBody2">14sp</item>
-        <item name="TextSizeSubhead">16sp</item>
-        <item name="TextSizeTitle">20sp</item>
-        <item name="TextSizeDisplay2">45sp</item>
-        <item name="TextSizeInput">16sp</item>
-        <item name="TextSeparation">5sp</item>
-        <item name="IconSize">18sp</item>
-
-        <item name="divider">@color/white12</item>
-
-        <item name="ic_send_cancel_offline" type="reference">
-            @drawable/ic_send_cancel_offline_white
-        </item>
-        <item name="ic_send_location_offline" type="reference">
-            @drawable/ic_send_location_offline_white
-        </item>
-        <item name="ic_send_photo_offline" type="reference">@drawable/ic_send_photo_offline_white
-        </item>
-        <item name="ic_send_picture_offline" type="reference">
-            @drawable/ic_send_picture_offline_white
-        </item>
-        <item name="ic_send_text_offline" type="reference">@drawable/ic_send_text_offline_white
-        </item>
-        <item name="ic_send_videocam_offline" type="reference">
-            @drawable/ic_send_videocam_offline_white
-        </item>
-        <item name="ic_send_voice_offline" type="reference">@drawable/ic_send_voice_offline_white
-        </item>
-
-        <item name="ic_make_audio_call" type="reference">@drawable/ic_call_white70_24dp</item>
-        <item name="ic_make_video_call" type="reference">@drawable/ic_videocam_white70_24dp</item>
-
-        <item name="ic_attach_camera" type="reference">@drawable/ic_attach_camera_white</item>
-        <item name="ic_attach_videocam" type="reference">@drawable/ic_attach_videocam_white</item>
-        <item name="ic_attach_document" type="reference">@drawable/ic_attach_document_white</item>
-        <item name="ic_attach_location" type="reference">@drawable/ic_attach_location_white</item>
-        <item name="ic_attach_photo" type="reference">@drawable/ic_attach_photo_white</item>
-        <item name="ic_attach_record" type="reference">@drawable/ic_attach_record_white</item>
-
-        <item name="unread_count">?colorPrimary</item>
-
-        <item name="conversations_overview_background">?colorPrimary</item>
-
-        <item name="icon_alpha" type="float">0.7</item>
-        <item name="delete_icon_alpha" type="float">0.7</item>
-
-        <item name="dialog_horizontal_padding">24dp</item>
-        <item name="dialog_vertical_padding">16dp</item>
-
-        <item name="media_preview_image" type="reference">@drawable/ic_image_white_48dp</item>
-        <item name="media_preview_document" type="reference">@drawable/ic_description_white_48dp</item>
-        <item name="media_preview_recording" type="reference">@drawable/ic_mic_white_48dp</item>
-        <item name="media_preview_audio" type="reference">@drawable/ic_headset_white_48dp</item>
-        <item name="media_preview_location" type="reference">@drawable/ic_room_white_48dp</item>
-        <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_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>
-        <item name="media_preview_backup" type="reference">@drawable/ic_backup_white_48dp</item>
-        <item name="media_preview_unknown" type="reference">@drawable/ic_help_white_48dp</item>
-
-        <item name="icon_link" type="reference">@drawable/link_white</item>
-        <item name="icon_email" type="reference">@drawable/email_white</item>
-        <item name="icon_org" type="reference">@drawable/business_white</item>
-        <item name="icon_chat" type="reference">@drawable/ic_chat_white_24dp</item>
-        <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>
-        <item name="icon_edit_body" type="reference">@drawable/ic_edit_white_24dp</item>
-        <item name="icon_save" type="reference">@drawable/ic_save_white_24dp</item>
-        <item name="icon_group" type="reference">@drawable/ic_group_white_24dp</item>
-        <item name="icon_new" type="reference">@drawable/ic_add_white_24dp</item>
-        <item name="icon_quote" type="reference">@drawable/ic_reply_white_24dp</item>
-        <item name="icon_refresh" type="reference">@drawable/ic_refresh_white_24dp</item>
-        <item name="icon_new_attachment" type="reference">@drawable/ic_attach_file_white_24dp</item>
-        <item name="icon_not_secure" type="reference">@drawable/ic_lock_open_white_24dp</item>
-        <item name="icon_call" type="reference">@drawable/ic_call_white_24dp</item>
-        <item name="icon_ongoing_call" type="reference">@drawable/ic_phone_in_talk_white_24dp</item>
-        <item name="ic_ongoing_call_hint" type="reference">@drawable/ic_phone_in_talk_white_18dp
-        </item>
-        <item name="icon_remove" type="reference">@drawable/ic_delete_white_24dp</item>
-        <item name="icon_search" type="reference">@drawable/ic_search_white_24dp</item>
-        <item name="icon_help" type="reference">@drawable/ic_help_white_24dp</item>
-        <item name="icon_goto_chat" type="reference">@drawable/ic_question_answer_white_24dp</item>
-        <item name="icon_secure" type="reference">@drawable/ic_lock_open_white_24dp</item>
-        <item name="icon_small_lock" type="reference">@drawable/ic_lock_white_18dp</item>
-        <item name="icon_settings" type="reference">@drawable/ic_settings_white_24dp</item>
-        <item name="icon_share" type="reference">@drawable/ic_share_white_24dp</item>
-        <item name="ic_cloud_download" type="reference">@drawable/ic_cloud_download_white_24dp
-        </item>
-        <item name="icon_scan_qr_code" type="reference">@drawable/ic_qr_code_scan_white_24dp</item>
-        <item name="icon_scroll_down" type="reference">@drawable/ic_scroll_to_end_white</item>
-
-        <item name="icon_gps_not_fixed" type="reference">@drawable/ic_gps_not_fixed_white_24dp
-        </item>
-        <item name="icon_gps_fixed" type="reference">@drawable/ic_gps_fixed_white_24dp</item>
-        <item name="icon_directions" type="reference">@drawable/ic_directions_white_24dp</item>
-        <item name="icon_copy_bar" type="reference">@drawable/ic_content_copy_white_24dp</item>
-
-        <item name="icon_notifications" type="reference">@drawable/ic_notifications_white_24dp
-        </item>
-        <item name="icon_notifications_off" type="reference">
-            @drawable/ic_notifications_off_white_24dp
-        </item>
-        <item name="icon_notifications_paused" type="reference">
-            @drawable/ic_notifications_paused_white_24dp
-        </item>
-        <item name="icon_notifications_none" type="reference">
-            @drawable/ic_notifications_none_white_24dp
-        </item>
-        <item name="icon_pinned_on_top" type="reference">@drawable/ic_star_white_24dp</item>
-        <item name="icon_enable_undecided_device" type="reference">
-            @drawable/ic_new_releases_white_24dp
-        </item>
-    </style>
-
-    <style name="ConversationsTheme.Obsidian" parent="ConversationsTheme.Dark">
-        <item name="colorPrimary">@color/black_perpy</item>
-        <item name="colorPrimaryDark">@color/black_perpy</item>
-        <item name="colorAccent">@color/yeller</item>
-
-        <item name="message_bubble_received_bg">@color/perpy_desaturated</item>
-        <item name="message_bubble_sent_bg">?color_background_primary</item>
-        <item name="message_bubble_shadow_light">#00000000</item>
-        <item name="message_bubble_shadow_dark">#FF000000</item>
-        <item name="message_bubble_shadow_dark_top">#DD000000</item>
-
-        <item name="color_background_primary">#0E0020</item>
-        <item name="color_background_secondary">@color/black</item>
-        <item name="color_background_tertiary">@color/black_perpy</item>
-        <item name="color_background_overlay">@color/black26</item>
-
-        <item name="unread_count">?colorPrimary</item>
-    </style>
-
-    <style name="ConversationsTheme.OLEDBlack" parent="ConversationsTheme.Dark">
-        <item name="colorPrimary">@color/black</item>
-        <item name="colorPrimaryDark">@color/black</item>
-        <item name="colorAccent">@color/yeller</item>
-
-        <item name="message_bubble_received_bg">?colorPrimary</item>
-        <item name="message_bubble_sent_bg">?color_background_primary</item>
-        <item name="message_bubble_shadow_light">#00000000</item>
-        <item name="message_bubble_shadow_dark">#FF000000</item>
-        <item name="message_bubble_shadow_dark_top">#DD000000</item>
-
-        <item name="color_background_primary">@color/black</item>
-        <item name="color_background_secondary">@color/black</item>
-        <item name="color_background_tertiary">@color/black</item>
-        <item name="color_background_overlay">@color/black26</item>
-
-        <item name="unread_count">?colorPrimary</item>
-    </style>
-
-    <style name="ConversationsTheme.Custom" parent="ConversationsTheme">
-        <item name="colorPrimary">@color/custom_theme_primary</item>
-        <item name="color_background_primary">@color/custom_theme_background_primary</item>
-        <item name="color_background_secondary">@color/custom_theme_background_secondary</item>
-        <item name="color_background_tertiary">@color/custom_theme_background_tertiary</item>
-        <item name="colorPrimaryDark">@color/custom_theme_primary_dark</item>
-        <item name="colorAccent">@color/custom_theme_accent</item>
-        <item name="message_bubble_received_bg">?colorPrimary</item>
-    </style>
-
-    <style name="ConversationsTheme.CustomDark" parent="ConversationsTheme.Dark">
-        <item name="colorPrimary">@color/custom_dark_theme_primary</item>
-        <item name="colorPrimaryDark">@color/custom_dark_theme_primary_dark</item>
-        <item name="color_background_primary">@color/custom_dark_theme_background_primary</item>
-        <item name="color_background_secondary">@color/custom_dark_theme_background_secondary</item>
-        <item name="color_background_tertiary">@color/custom_dark_theme_background_tertiary</item>
-        <item name="colorAccent">@color/custom_dark_theme_accent</item>
-        <item name="message_bubble_received_bg">?colorPrimary</item>
-    </style>
-
-    <style name="ConversationsTheme.Medium" parent="ConversationsTheme">
-        <item name="TextSizeCaption">14sp</item>
-        <item name="TextSizeBody1">16sp</item>
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeSubhead">18sp</item>
-        <item name="TextSizeTitle">22sp</item>
-        <item name="TextSizeDisplay2">47sp</item>
-        <item name="TextSizeInput">18sp</item>
-        <item name="TextSeparation">6sp</item>
-        <item name="IconSize">20sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Dark.Medium" parent="ConversationsTheme.Dark">
-        <item name="TextSizeCaption">14sp</item>
-        <item name="TextSizeBody1">16sp</item>
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeSubhead">18sp</item>
-        <item name="TextSizeTitle">22sp</item>
-        <item name="TextSizeDisplay2">47sp</item>
-        <item name="TextSizeInput">18sp</item>
-        <item name="TextSeparation">6sp</item>
-        <item name="IconSize">20sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Obsidian.Medium" parent="ConversationsTheme.Obsidian">
-        <item name="TextSizeCaption">14sp</item>
-        <item name="TextSizeBody1">16sp</item>
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeSubhead">18sp</item>
-        <item name="TextSizeTitle">22sp</item>
-        <item name="TextSizeDisplay2">47sp</item>
-        <item name="TextSizeInput">18sp</item>
-        <item name="TextSeparation">6sp</item>
-        <item name="IconSize">20sp</item>
-    </style>
-
-    <style name="ConversationsTheme.CustomDark.Medium" parent="ConversationsTheme.CustomDark">
-        <item name="TextSizeCaption">14sp</item>
-        <item name="TextSizeBody1">16sp</item>
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeSubhead">18sp</item>
-        <item name="TextSizeTitle">22sp</item>
-        <item name="TextSizeDisplay2">47sp</item>
-        <item name="TextSizeInput">18sp</item>
-        <item name="TextSeparation">6sp</item>
-        <item name="IconSize">20sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Custom.Medium" parent="ConversationsTheme.Custom">
-        <item name="TextSizeCaption">14sp</item>
-        <item name="TextSizeBody1">16sp</item>
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeSubhead">18sp</item>
-        <item name="TextSizeTitle">22sp</item>
-        <item name="TextSizeDisplay2">47sp</item>
-        <item name="TextSizeInput">18sp</item>
-        <item name="TextSeparation">6sp</item>
-        <item name="IconSize">20sp</item>
-    </style>
-
-    <style name="ConversationsTheme.OLEDBlack.Medium" parent="ConversationsTheme.OLEDBlack">
-        <item name="TextSizeCaption">14sp</item>
-        <item name="TextSizeBody1">16sp</item>
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeSubhead">18sp</item>
-        <item name="TextSizeTitle">22sp</item>
-        <item name="TextSizeDisplay2">47sp</item>
-        <item name="TextSizeInput">18sp</item>
-        <item name="TextSeparation">6sp</item>
-        <item name="IconSize">20sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Dark.Large" parent="ConversationsTheme.Dark">
-        <item name="TextSizeCaption">16sp</item>
-        <item name="TextSizeBody1">18sp</item>
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeSubhead">20sp</item>
-        <item name="TextSizeTitle">24sp</item>
-        <item name="TextSizeDisplay2">48sp</item>
-        <item name="TextSizeInput">20sp</item>
-        <item name="TextSeparation">7sp</item>
-        <item name="IconSize">22sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Obsidian.Large" parent="ConversationsTheme.Obsidian">
-        <item name="TextSizeCaption">16sp</item>
-        <item name="TextSizeBody1">18sp</item>
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeSubhead">20sp</item>
-        <item name="TextSizeTitle">24sp</item>
-        <item name="TextSizeDisplay2">48sp</item>
-        <item name="TextSizeInput">20sp</item>
-        <item name="TextSeparation">7sp</item>
-        <item name="IconSize">22sp</item>
-    </style>
-
-    <style name="ConversationsTheme.OLEDBlack.Large" parent="ConversationsTheme.OLEDBlack">
-        <item name="TextSizeCaption">16sp</item>
-        <item name="TextSizeBody1">18sp</item>
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeSubhead">20sp</item>
-        <item name="TextSizeTitle">24sp</item>
-        <item name="TextSizeDisplay2">48sp</item>
-        <item name="TextSizeInput">20sp</item>
-        <item name="TextSeparation">7sp</item>
-        <item name="IconSize">22sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Custom.Large" parent="ConversationsTheme.Custom">
-        <item name="TextSizeCaption">16sp</item>
-        <item name="TextSizeBody1">18sp</item>
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeSubhead">20sp</item>
-        <item name="TextSizeTitle">24sp</item>
-        <item name="TextSizeDisplay2">48sp</item>
-        <item name="TextSizeInput">20sp</item>
-        <item name="TextSeparation">7sp</item>
-        <item name="IconSize">22sp</item>
-    </style>
-
-    <style name="ConversationsTheme.CustomDark.Large" parent="ConversationsTheme.CustomDark">
-        <item name="TextSizeCaption">16sp</item>
-        <item name="TextSizeBody1">18sp</item>
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeSubhead">20sp</item>
-        <item name="TextSizeTitle">24sp</item>
-        <item name="TextSizeDisplay2">48sp</item>
-        <item name="TextSizeInput">20sp</item>
-        <item name="TextSeparation">7sp</item>
-        <item name="IconSize">22sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Large" parent="ConversationsTheme">
-        <item name="TextSizeCaption">16sp</item>
-        <item name="TextSizeBody1">18sp</item>
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeSubhead">20sp</item>
-        <item name="TextSizeTitle">24sp</item>
-        <item name="TextSizeDisplay2">48sp</item>
-        <item name="TextSizeInput">20sp</item>
-        <item name="TextSeparation">7sp</item>
-        <item name="IconSize">22sp</item>
-    </style>
-
-    <style name="ConversationsTheme.FullScreen" parent="@style/Theme.AppCompat.Light">
-        <item name="colorPrimary">@color/green600</item>
-        <item name="colorPrimaryDark">@color/green700</item>
-        <item name="android:windowNoTitle">true</item>
-        <item name="android:windowActionBar">false</item>
-        <item name="android:windowFullscreen">true</item>
-        <item name="android:windowContentOverlay">@null</item>
-        <item name="android:windowBackground">@android:color/black</item>
-        <item name="android:navigationBarColor" tools:targetApi="21">@color/black</item>
-    </style>
-
-    <style name="SplashTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
-        <item name="colorPrimaryDark">@color/splash_screen_status_bar</item>
-        <item name="android:windowBackground">@drawable/background</item>
-    </style>
-
-    <style name="ConversationsTheme.Dialog" parent="@style/Theme.AppCompat.Light.Dialog">
-        <item name="colorPrimary">@color/green600</item>
-        <item name="colorPrimaryDark">@color/green700</item>
-        <item name="colorAccent">@color/blue_a400</item>
-        <item name="color_background_primary">@color/grey50</item>
-        <item name="divider">@color/black12</item>
-        <item name="TextSizeTitle">18sp</item>
-        <item name="TextSizeBody2">14sp</item>
-        <item name="TextSizeDisplay2">45sp</item>
-        <item name="android:windowNoTitle">true</item>
-    </style>
-
-    <style name="ConversationsTheme.Dark.Dialog" parent="@style/Theme.AppCompat.Dialog">
-        <item name="colorPrimary">@color/green800</item>
-        <item name="colorPrimaryDark">@color/green900</item>
-        <item name="colorAccent">@color/blue_a100</item>
-        <item name="color_background_primary">@color/grey800</item>
-        <item name="divider">@color/white12</item>
-        <item name="TextSizeBody2">14sp</item>
-        <item name="TextSizeDisplay2">45sp</item>
-        <item name="android:windowNoTitle">true</item>
-    </style>
-
-    <style name="ConversationsTheme.Dialog.Medium" parent="ConversationsTheme.Dialog">
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeDisplay2">51sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Dark.Dialog.Medium" parent="ConversationsTheme.Dark.Dialog">
-        <item name="TextSizeBody2">16sp</item>
-        <item name="TextSizeDisplay2">51sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Dialog.Large" parent="ConversationsTheme.Dialog">
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeDisplay2">56sp</item>
-    </style>
-
-    <style name="ConversationsTheme.Dark.Dialog.Large" parent="ConversationsTheme.Dark.Dialog">
-        <item name="TextSizeBody2">18sp</item>
-        <item name="TextSizeTitle">56sp</item>
-    </style>
-
-</resources>

src/conversations/AndroidManifest.xml 🔗

@@ -5,6 +5,7 @@
         <activity
             android:name=".ui.ManageAccountActivity"
             android:label="@string/title_activity_manage_accounts"
+            android:theme="@style/Theme.Conversations3"
             android:launchMode="singleTask" />
         <activity
             android:name=".ui.WelcomeActivity"

src/conversations/fastlane/metadata/android/fr-FR/full_description.txt 🔗

@@ -0,0 +1,38 @@
+Facile à utiliser, fiable, respectueux de votre batterie. Prend en charge les images, les conversations de groupe et le chiffrement de bout-en-bout.
+
+Principes de conception :
+
+* Être le plus joli et simple d'utilisation possible, sans compromis sur la sécurité ou la vie privée.
+* S'appuyer sur des protocoles existants, bien établis
+* Ne pas nécessiter de compte Google ou spécifiquement le Google Cloud Messaging (GCM)
+* Nécessiter le moins de permissions possible
+
+Fonctionnalités :
+* Chiffrement de bout-en-bout avec au choix, <a href="http://conversations.im/omemo/">OMEMO</a> ou <a href="http://openpgp.org/about/">OpenPGP</a>
+* Envoi et réception d'images
+* Appels audio et vidéo chiffrés (DTLS-SRTP)
+* Interface utilisateur intuitive qui suit les directives de conception d'Android
+* Images / avatars pour vos contacts
+* Synchronisation avec des clients de bureau
+* Conférences (avec prise en charge des marque-pages)
+* Intégration avec le carnet d'adresses
+* Plusieurs comptes / boîte de réception unifiée
+* Impact très faible sur l'autonomie de la batterie
+
+Conversations facilite la création de compte sur le serveur gratuit conversations.im. Cependant Conversations fonctionne également avec n'importe quel autre serveur XMPP. De nombreux serveurs XMPP sont gérés par des bénévoles et gratuits.
+
+Fonctionnalités de XMPP :
+
+Conversations fonctionne avec n'importe quel serveur XMPP. Cependant XMPP est un protocole extensible. Ces extensions sont aussi standardisées dans ce que l'on appelle les XEP. Conversations en prend en charge quelques-unes pour rendre l'expérience utilisateur meilleure dans l'ensemble. Il y a des chances que votre serveur XMPP actuel ne les prenne pas en charge. Ainsi, pour tirer le meilleur parti de Conversations, vous devriez envisager soit de passer à un serveur XMPP qui le fait, ou encore mieux, gérer votre propre serveur XMPP pour vous et vos amis.
+
+Ces XEP sont actuellement :
+
+* XEP-0065: SOCKS5 Bytestreams (ou mod_proxy65). Sera utilisé pour transférer des fichiers si les deux correspondants sont derrière un pare-feu (NAT).
+* XEP-0163: Personal Eventing Protocol pour les avatars
+* XEP-0191: Blocking Command vous permet de mettre des spammeurs sur liste noire ou bloquer des contacts sans les retirer de vos contacts.
+* XEP-0198: Stream Management permet à XMPP de survivre à des petites pannes de réseau et aux changements de la connexion TCP sous-jacente.
+* XEP-0280: Message Carbons qui synchronise automatiquement les messages que vous envoyez à votre client de bureau et vous permet ainsi de passer sans heurt de votre client mobile à votre client de bureau et inversement dans une conversation.
+* XEP-0237: Roster Versioning principalement pour économiser de la bande passante sur les connexions mobiles de mauvaise qualité.
+* XEP-0313: Message Archive Management synchronise l'historique des messages avec le serveur. Retrouvez des messages qui ont été envoyés pendant que Conversations était hors ligne.
+* XEP-0352: Client State Indication fait savoir au serveur si Conversations est ou n'est pas en arrière-plan. Permet au serveur d'économiser de la bande passante en différant des paquets non importants.
+* XEP-0363: HTTP File Upload vous permet de partager des fichiers dans les conférences et avec des contacts hors-ligne. Nécessite un composant supplémentaire sur votre serveur.

src/conversations/fastlane/metadata/android/ru-RU/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
+* Картинки / аватары для ваших контактов
+* Синхронизация с настольным клиентом
+* Конференции (с поддержкой закладок)
+* Интеграция с адресной книгой
+* Несколько учетных записей / единый почтовый ящик
+* Очень низкое влияние на время работы от батареи
+
+Conversations позволяет очень легко создать учетную запись на бесплатном сервере conversations.im. Однако Conversations также будет работать с любым другим сервером XMPP. Многие XMPP серверы управляются добровольцами и предоставляются бесплатно.
+
+Возможности XMPP:
+

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

@@ -1,4 +1,4 @@
-易于使用、性能可靠、电池友好。内置支持图片、群聊和 e2e 加密功能。
+易用、可靠、省电。内置支持图片、群聊和端到端加密功能。
 
 设计原则:
 
@@ -9,7 +9,7 @@
 
 特点:
 
-* 使用 <a href="http://conversations.im/omemo/">OMEMO</a> 或 <a href="http://openpgp.org/about/">OpenPGP</a> 进行端对端加密
+* 使用 <a href="http://conversations.im/omemo/">OMEMO</a> 或 <a href="http://openpgp.org/about/">OpenPGP</a> 进行端到端加密
 * 发送和接收图片
 * 加密音视频通话(DTLS-SRTP)
 * 直观的用户界面,遵循 Android 设计准则
@@ -24,9 +24,9 @@ Conversations 使在免费的 conversations.im 服务器上创建账号变得非
 
 XMPP 功能:
 
-Conversations 适用于所有 XMPP 服务器。然而,XMPP 是一种可扩展的协议。这些扩展在所谓的 XEP 中也是标准化的。Conversations 支持其中的一些扩展,以使整体用户体验更好。有一种可能是您当前的 XMPP 服务器不支持这些扩展。因此,要想充分使用 Conversations 的功能,您应该考虑切换到支持这些扩展的 XMPP 服务器,甚至有更好的方式,或者为您和您的朋友运行自己的 XMPP 服务器。
+Conversations 适用于所有 XMPP 服务器。然而,XMPP 是一种可扩展的协议。这些扩展在所谓的 XMPP 扩展协议中也是标准化的。Conversations 支持其中的一些扩展,以使整体用户体验更好。有一种可能是您当前的 XMPP 服务器不支持这些扩展。因此,要想充分使用 Conversations 的功能,您应该考虑切换到支持这些扩展的 XMPP 服务器,甚至有更好的方式,或者为您和您的朋友运行自己的 XMPP 服务器。
 
-到目前为止,这些 XEP 是:
+到目前为止,这些 XMPP 扩展协议是:
 
 * XEP-0065:SOCKS5 字节流(或 mod_proxy65)。如果双方都在防火墙(NAT)后面,将用于传输文件。
 * XEP-0163:个人事件协议(头像)

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

@@ -58,6 +58,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -188,12 +189,7 @@ public class ImportBackupService extends Service {
                         }
                     }
                     Collections.sort(
-                            backupFiles,
-                            (a, b) ->
-                                    a.header
-                                            .getJid()
-                                            .toString()
-                                            .compareTo(b.header.getJid().toString()));
+                            backupFiles, Comparator.comparing(a -> a.header.getJid().toString()));
                     onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
                 });
     }
@@ -225,7 +221,7 @@ public class ImportBackupService extends Service {
         NotificationCompat.Builder mBuilder =
                 new NotificationCompat.Builder(getBaseContext(), "backup");
         mBuilder.setContentTitle(getString(R.string.restoring_backup))
-                .setSmallIcon(R.drawable.ic_unarchive_white_24dp)
+                .setSmallIcon(R.drawable.ic_unarchive_24dp)
                 .setProgress(max, progress, max == 1 && progress == 0);
         return mBuilder.build();
     }
@@ -415,7 +411,7 @@ public class ImportBackupService extends Service {
                                         ? PendingIntent.FLAG_IMMUTABLE
                                                 | PendingIntent.FLAG_UPDATE_CURRENT
                                         : PendingIntent.FLAG_UPDATE_CURRENT))
-                .setSmallIcon(R.drawable.ic_unarchive_white_24dp);
+                .setSmallIcon(R.drawable.ic_unarchive_24dp);
         notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
     }
 

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

@@ -2,6 +2,7 @@ package eu.siacs.conversations.ui;
 
 import android.app.Activity;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.os.Bundle;
@@ -11,8 +12,10 @@ import android.view.MenuItem;
 import android.view.View;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.color.MaterialColors;
 import com.google.common.base.Strings;
 
 import eu.siacs.conversations.Config;
@@ -23,19 +26,20 @@ import eu.siacs.conversations.services.BarcodeProvider;
 import eu.siacs.conversations.utils.EasyOnboardingInvite;
 import eu.siacs.conversations.xmpp.Jid;
 
-public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOnboardingInvite.OnInviteRequested {
+public class EasyOnboardingInviteActivity extends XmppActivity
+        implements EasyOnboardingInvite.OnInviteRequested {
 
     private ActivityEasyInviteBinding binding;
 
     private EasyOnboardingInvite easyOnboardingInvite;
 
-
     @Override
     public void onCreate(final Bundle bundle) {
         super.onCreate(bundle);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite);
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar(), true);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         this.binding.shareButton.setOnClickListener(v -> share());
         if (bundle != null && bundle.containsKey("invite")) {
             this.easyOnboardingInvite = bundle.getParcelable("invite");
@@ -65,11 +69,11 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn
     }
 
     private void share() {
-        final String shareText = getString(
-                R.string.easy_invite_share_text,
-                easyOnboardingInvite.getDomain(),
-                easyOnboardingInvite.getShareableLink()
-        );
+        final String shareText =
+                getString(
+                        R.string.easy_invite_share_text,
+                        easyOnboardingInvite.getDomain(),
+                        easyOnboardingInvite.getShareableLink());
         final Intent sendIntent = new Intent();
         sendIntent.setAction(Intent.ACTION_SEND);
         sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
@@ -95,16 +99,47 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn
     private void showInvite(final EasyOnboardingInvite invite) {
         this.binding.inProgress.setVisibility(View.GONE);
         this.binding.invite.setVisibility(View.VISIBLE);
-        this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain()));
+        this.binding.tapToShare.setText(
+                getString(R.string.tap_share_button_send_invite, invite.getDomain()));
         final Point size = new Point();
         getWindowManager().getDefaultDisplay().getSize(size);
         final int width = Math.min(size.x, size.y);
-        final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableLink(), width);
+        final boolean nightMode =
+                (this.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+                        == Configuration.UI_MODE_NIGHT_YES;
+        final int black;
+        final int white;
+        if (nightMode) {
+            black =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurface,
+                            "No surface color configured");
+            white =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurfaceInverse,
+                            "No inverse surface color configured");
+        } else {
+            black =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurfaceInverse,
+                            "No inverse surface color configured");
+            white =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurface,
+                            "No surface color configured");
+        }
+        final Bitmap bitmap =
+                BarcodeProvider.create2dBarcodeBitmap(
+                        invite.getShareableLink(), width, black, white);
         binding.qrCode.setImageBitmap(bitmap);
     }
 
     @Override
-    public void onSaveInstanceState(Bundle bundle) {
+    public void onSaveInstanceState(@NonNull Bundle bundle) {
         super.onSaveInstanceState(bundle);
         if (easyOnboardingInvite != null) {
             bundle.putParcelable("invite", easyOnboardingInvite);
@@ -112,7 +147,7 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         if (easyOnboardingInvite != null) {
             return;
         }
@@ -141,11 +176,12 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn
 
     @Override
     public void inviteRequestFailed(final String message) {
-        runOnUiThread(() -> {
-            if (!Strings.isNullOrEmpty(message)) {
-                Toast.makeText(this, message, Toast.LENGTH_LONG).show();
-            }
-            finish();
-        });
+        runOnUiThread(
+                () -> {
+                    if (!Strings.isNullOrEmpty(message)) {
+                        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+                    }
+                    finish();
+                });
     }
 }

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

@@ -1,11 +1,14 @@
 package eu.siacs.conversations.ui;
 
+import android.Manifest;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.util.Log;
@@ -14,14 +17,16 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.snackbar.Snackbar;
-
-import java.io.IOException;
-import java.util.List;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -29,11 +34,18 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
 import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
 import eu.siacs.conversations.services.ImportBackupService;
 import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
-import eu.siacs.conversations.ui.util.SettingsUtils;
 import eu.siacs.conversations.utils.BackupFileHeader;
-import eu.siacs.conversations.utils.ThemeHelper;
 
-public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class ImportBackupActivity extends ActionBarActivity
+        implements ServiceConnection,
+                ImportBackupService.OnBackupFilesLoaded,
+                BackupFileAdapter.OnItemClickedListener,
+                ImportBackupService.OnBackupProcessed {
 
     private ActivityImportBackupBinding binding;
 
@@ -41,27 +53,32 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
     private ImportBackupService service;
 
     private boolean mLoadingState = false;
-
-    private int mTheme;
+    private final ActivityResultLauncher<String[]> requestPermissions =
+            registerForActivityResult(
+                    new ActivityResultContracts.RequestMultiplePermissions(),
+                    results -> {
+                        if (results.containsValue(Boolean.TRUE)) {
+                            final var service = this.service;
+                            if (service == null) {
+                                return;
+                            }
+                            service.loadBackupFiles(this);
+                        }
+                    });
 
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
-        this.mTheme = ThemeHelper.find(this);
-        setTheme(this.mTheme);
         super.onCreate(savedInstanceState);
         binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
-        setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false));
+        setLoadingState(
+                savedInstanceState != null
+                        && savedInstanceState.getBoolean("loading_state", false));
         this.backupFileAdapter = new BackupFileAdapter();
         this.binding.list.setAdapter(this.backupFileAdapter);
         this.backupFileAdapter.setOnItemClickedListener(this);
     }
-    
-    @Override
-    protected void onResume(){
-        super.onResume();
-        SettingsUtils.applyScreenshotPreventionSetting(this);
-    }
 
     @Override
     public boolean onCreateOptionsMenu(final Menu menu) {
@@ -79,20 +96,55 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     @Override
     public void onStart() {
+
         super.onStart();
-        final int theme = ThemeHelper.find(this);
-        if (this.mTheme != theme) {
-            recreate();
-        } else {
-            bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
-        }
+        bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
         final Intent intent = getIntent();
-        if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) {
+        if (intent != null
+                && Intent.ACTION_VIEW.equals(intent.getAction())
+                && !this.mLoadingState) {
             Uri uri = intent.getData();
             if (uri != null) {
                 openBackupFileFromUri(uri, true);
+                return;
             }
         }
+        final List<String> desiredPermission;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            desiredPermission =
+                    ImmutableList.of(
+                            Manifest.permission.READ_MEDIA_IMAGES,
+                            Manifest.permission.READ_MEDIA_VIDEO,
+                            Manifest.permission.READ_MEDIA_AUDIO,
+                            Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED);
+        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
+            desiredPermission =
+                    ImmutableList.of(
+                            Manifest.permission.READ_MEDIA_IMAGES,
+                            Manifest.permission.READ_MEDIA_VIDEO,
+                            Manifest.permission.READ_MEDIA_AUDIO);
+        } else {
+            desiredPermission = ImmutableList.of(Manifest.permission.READ_EXTERNAL_STORAGE);
+        }
+        final Set<String> declaredPermission = getDeclaredPermission();
+        if (declaredPermission.containsAll(desiredPermission)) {
+            requestPermissions.launch(desiredPermission.toArray(new String[0]));
+        } else {
+            Log.d(Config.LOGTAG, "Manifest is lacking some desired permission. not requesting");
+        }
+    }
+
+    private Set<String> getDeclaredPermission() {
+        final String[] permissions;
+        try {
+            permissions =
+                    getPackageManager()
+                            .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS)
+                            .requestedPermissions;
+        } catch (final PackageManager.NameNotFoundException e) {
+            return Collections.emptySet();
+        }
+        return ImmutableSet.copyOf(permissions);
     }
 
     @Override
@@ -106,7 +158,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
-        ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service;
+        ImportBackupService.ImportBackupServiceBinder binder =
+                (ImportBackupService.ImportBackupServiceBinder) service;
         this.service = binder.getService();
         this.service.addOnBackupProcessedListener(this);
         setLoadingState(this.service.getLoadingState());
@@ -130,55 +183,81 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) {
         try {
-            final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
+            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();
+            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();
+            Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG)
+                    .show();
         } catch (final SecurityException e) {
-            Snackbar.make(binding.coordinator, R.string.sharing_application_not_grant_permission, Snackbar.LENGTH_LONG).show();
+            Snackbar.make(
+                            binding.coordinator,
+                            R.string.sharing_application_not_grant_permission,
+                            Snackbar.LENGTH_LONG)
+                    .show();
         }
     }
 
-    private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) {
-        final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
+    private void showEnterPasswordDialog(
+            final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) {
+        final DialogEnterPasswordBinding enterPasswordBinding =
+                DataBindingUtil.inflate(
+                        LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
         Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri());
-        enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString()));
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        enterPasswordBinding.explain.setText(
+                getString(
+                        R.string.enter_password_to_restore,
+                        backupFile.getHeader().getJid().toString()));
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setView(enterPasswordBinding.getRoot());
         builder.setTitle(R.string.enter_password);
-        builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
-            if (finishOnCancel) {
-                finish();
-            }
-        });
+        builder.setNegativeButton(
+                R.string.cancel,
+                (dialog, which) -> {
+                    if (finishOnCancel) {
+                        finish();
+                    }
+                });
         builder.setPositiveButton(R.string.restore, null);
         builder.setCancelable(false);
         final AlertDialog dialog = builder.create();
-        dialog.setOnShowListener((d) -> {
-            dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
-                final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
-                if (password.isEmpty()) {
-                    enterPasswordBinding.accountPasswordLayout.setError(getString(R.string.please_enter_password));
-                    return;
-                }
-                final Uri uri = backupFile.getUri();
-                Intent intent = new Intent(this, ImportBackupService.class);
-                intent.setAction(Intent.ACTION_SEND);
-                intent.putExtra("password", password);
-                if ("file".equals(uri.getScheme())) {
-                    intent.putExtra("file", uri.getPath());
-                } else {
-                    intent.setData(uri);
-                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-                }
-                setLoadingState(true);
-                ContextCompat.startForegroundService(this, intent);
-                d.dismiss();
-            });
-        });
+        dialog.setOnShowListener(
+                (d) -> {
+                    dialog.getButton(DialogInterface.BUTTON_POSITIVE)
+                            .setOnClickListener(
+                                    v -> {
+                                        final String password =
+                                                enterPasswordBinding
+                                                        .accountPassword
+                                                        .getEditableText()
+                                                        .toString();
+                                        if (password.isEmpty()) {
+                                            enterPasswordBinding.accountPasswordLayout.setError(
+                                                    getString(R.string.please_enter_password));
+                                            return;
+                                        }
+                                        final Uri uri = backupFile.getUri();
+                                        Intent intent = new Intent(this, ImportBackupService.class);
+                                        intent.setAction(Intent.ACTION_SEND);
+                                        intent.putExtra("password", password);
+                                        if ("file".equals(uri.getScheme())) {
+                                            intent.putExtra("file", uri.getPath());
+                                        } else {
+                                            intent.setData(uri);
+                                            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                                        }
+                                        setLoadingState(true);
+                                        ContextCompat.startForegroundService(this, intent);
+                                        d.dismiss();
+                                    });
+                });
         dialog.show();
     }
 
@@ -186,6 +265,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
         binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE);
         binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE);
         setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         configureActionBar(getSupportActionBar(), !loadingState);
         this.mLoadingState = loadingState;
         invalidateOptionsMenu();
@@ -203,36 +283,55 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     @Override
     public void onAccountAlreadySetup() {
-        runOnUiThread(() -> {
-            setLoadingState(false);
-            Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show();
-        });
+        runOnUiThread(
+                () -> {
+                    setLoadingState(false);
+                    Snackbar.make(
+                                    binding.coordinator,
+                                    R.string.account_already_setup,
+                                    Snackbar.LENGTH_LONG)
+                            .show();
+                });
     }
 
     @Override
     public void onBackupRestored() {
-        runOnUiThread(() -> {
-            Intent intent = new Intent(this, ConversationActivity.class);
-            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-            startActivity(intent);
-            finish();
-        });
+        runOnUiThread(
+                () -> {
+                    Intent intent = new Intent(this, ConversationActivity.class);
+                    intent.addFlags(
+                            Intent.FLAG_ACTIVITY_CLEAR_TOP
+                                    | Intent.FLAG_ACTIVITY_NEW_TASK
+                                    | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                    startActivity(intent);
+                    finish();
+                });
     }
 
     @Override
     public void onBackupDecryptionFailed() {
-        runOnUiThread(() -> {
-            setLoadingState(false);
-            Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show();
-        });
+        runOnUiThread(
+                () -> {
+                    setLoadingState(false);
+                    Snackbar.make(
+                                    binding.coordinator,
+                                    R.string.unable_to_decrypt_backup,
+                                    Snackbar.LENGTH_LONG)
+                            .show();
+                });
     }
 
     @Override
     public void onBackupRestoreFailed() {
-        runOnUiThread(() -> {
-            setLoadingState(false);
-            Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show();
-        });
+        runOnUiThread(
+                () -> {
+                    setLoadingState(false);
+                    Snackbar.make(
+                                    binding.coordinator,
+                                    R.string.unable_to_restore_backup,
+                                    Snackbar.LENGTH_LONG)
+                            .show();
+                });
     }
 
     @Override
@@ -249,6 +348,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
         intent.setType("*/*");
         intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
         intent.addCategory(Intent.CATEGORY_OPENABLE);
-        startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
+        startActivityForResult(
+                Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
     }
 }

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

@@ -10,45 +10,32 @@ import android.widget.Toast;
 
 import androidx.databinding.DataBindingUtil;
 
-import java.security.SecureRandom;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.MagicCreateBinding;
+import eu.siacs.conversations.databinding.ActivityMagicCreateBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.InstallReferrerUtils;
 import eu.siacs.conversations.xmpp.Jid;
 
+import java.security.SecureRandom;
+
 public class MagicCreateActivity extends XmppActivity implements TextWatcher {
 
     public static final String EXTRA_DOMAIN = "domain";
     public static final String EXTRA_PRE_AUTH = "pre_auth";
     public static final String EXTRA_USERNAME = "username";
 
-    private MagicCreateBinding binding;
+    private ActivityMagicCreateBinding binding;
     private String domain;
     private String username;
     private String preAuth;
 
     @Override
-    protected void refreshUiReal() {
-
-    }
+    protected void refreshUiReal() {}
 
     @Override
-    void onBackendConnected() {
-
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
-    }
+    protected void onBackendConnected() {}
 
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
@@ -60,7 +47,8 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         }
         super.onCreate(savedInstanceState);
-        this.binding = DataBindingUtil.setContentView(this, R.layout.magic_create);
+        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_magic_create);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(this.binding.toolbar);
         configureActionBar(getSupportActionBar(), this.domain == null);
         if (username != null && domain != null) {
@@ -72,51 +60,64 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
         } else if (domain != null) {
             binding.instructions.setText(getString(R.string.magic_create_text_on_x, domain));
         }
-        binding.createAccount.setOnClickListener(v -> {
-            try {
-                final String username = binding.username.getText().toString();
-                final Jid jid;
-                final boolean fixedUsername;
-                if (this.domain != null && this.username != null) {
-                    fixedUsername = true;
-                    jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain);
-                } else if (this.domain != null) {
-                    fixedUsername = false;
-                    jid = Jid.ofLocalAndDomainEscaped(username, this.domain);
-                } else {
-                    fixedUsername = false;
-                    jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN);
-                }
-                if (!jid.getEscapedLocal().equals(jid.getLocal()) || (this.username == null && username.length() < 3)) {
-                    binding.username.setError(getString(R.string.invalid_username));
-                    binding.username.requestFocus();
-                } else {
-                    binding.username.setError(null);
-                    Account account = xmppConnectionService.findAccountByJid(jid);
-                    if (account == null) {
-                        account = new Account(jid, CryptoHelper.createPassword(new SecureRandom()));
-                        account.setOption(Account.OPTION_REGISTER, true);
-                        account.setOption(Account.OPTION_DISABLED, true);
-                        account.setOption(Account.OPTION_MAGIC_CREATE, true);
-                        account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername);
-                        if (this.preAuth != null) {
-                            account.setKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth);
+        binding.createAccount.setOnClickListener(
+                v -> {
+                    try {
+                        final String username = binding.username.getText().toString();
+                        final Jid jid;
+                        final boolean fixedUsername;
+                        if (this.domain != null && this.username != null) {
+                            fixedUsername = true;
+                            jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain);
+                        } else if (this.domain != null) {
+                            fixedUsername = false;
+                            jid = Jid.ofLocalAndDomainEscaped(username, this.domain);
+                        } else {
+                            fixedUsername = false;
+                            jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN);
+                        }
+                        if (!jid.getEscapedLocal().equals(jid.getLocal())
+                                || (this.username == null && username.length() < 3)) {
+                            binding.usernameLayout.setError(getString(R.string.invalid_username));
+                            binding.username.requestFocus();
+                        } else {
+                            binding.usernameLayout.setError(null);
+                            Account account = xmppConnectionService.findAccountByJid(jid);
+                            if (account == null) {
+                                account =
+                                        new Account(
+                                                jid,
+                                                CryptoHelper.createPassword(new SecureRandom()));
+                                account.setOption(Account.OPTION_REGISTER, true);
+                                account.setOption(Account.OPTION_DISABLED, true);
+                                account.setOption(Account.OPTION_MAGIC_CREATE, true);
+                                account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername);
+                                if (this.preAuth != null) {
+                                    account.setKey(
+                                            Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth);
+                                }
+                                xmppConnectionService.createAccount(account);
+                            }
+                            Intent intent =
+                                    new Intent(MagicCreateActivity.this, EditAccountActivity.class);
+                            intent.putExtra("jid", account.getJid().asBareJid().toString());
+                            intent.putExtra("init", true);
+                            intent.setFlags(
+                                    Intent.FLAG_ACTIVITY_NEW_TASK
+                                            | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                            Toast.makeText(
+                                            MagicCreateActivity.this,
+                                            R.string.secure_password_generated,
+                                            Toast.LENGTH_SHORT)
+                                    .show();
+                            StartConversationActivity.addInviteUri(intent, getIntent());
+                            startActivity(intent);
                         }
-                        xmppConnectionService.createAccount(account);
+                    } catch (final IllegalArgumentException e) {
+                        binding.usernameLayout.setError(getString(R.string.invalid_username));
+                        binding.username.requestFocus();
                     }
-                    Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class);
-                    intent.putExtra("jid", account.getJid().asBareJid().toString());
-                    intent.putExtra("init", true);
-                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-                    Toast.makeText(MagicCreateActivity.this, R.string.secure_password_generated, Toast.LENGTH_SHORT).show();
-                    StartConversationActivity.addInviteUri(intent, getIntent());
-                    startActivity(intent);
-                }
-            } catch (IllegalArgumentException e) {
-                binding.username.setError(getString(R.string.invalid_username));
-                binding.username.requestFocus();
-            }
-        });
+                });
         binding.username.addTextChangedListener(this);
     }
 
@@ -127,14 +128,10 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
     }
 
     @Override
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
-    }
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
 
     @Override
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-
-    }
+    public void onTextChanged(CharSequence s, int start, int before, int count) {}
 
     @Override
     public void afterTextChanged(final Editable s) {
@@ -153,8 +150,10 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
                 } else {
                     jid = Jid.ofLocalAndDomainEscaped(username, this.domain);
                 }
-                binding.fullJid.setText(getString(R.string.your_full_jid_will_be, jid.toEscapedString()));
-            } catch (IllegalArgumentException e) {
+                binding.fullJid.setText(
+                        getString(R.string.your_full_jid_will_be, jid.toEscapedString()));
+                binding.usernameLayout.setError(null);
+            } catch (final IllegalArgumentException e) {
                 binding.fullJid.setVisibility(View.INVISIBLE);
             }
         }

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

@@ -1,10 +1,14 @@
 package eu.siacs.conversations.ui;
 
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+
 import android.content.ActivityNotFoundException;
 import android.content.Intent;
 import android.os.Bundle;
 import android.security.KeyChain;
 import android.security.KeyChainAliasCallback;
+import android.util.Log;
 import android.util.Pair;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
@@ -12,23 +16,17 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.AdapterView.AdapterContextMenuInfo;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.ListView;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AlertDialog;
+import androidx.databinding.DataBindingUtil;
 
-import org.openintents.openpgp.util.OpenPgpApi;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
+import com.google.common.base.Strings;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityManageAccountsBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@@ -37,10 +35,17 @@ import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 
-import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
-import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+import org.openintents.openpgp.util.OpenPgpApi;
 
-public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ManageAccountActivity extends XmppActivity
+        implements OnAccountUpdate,
+                KeyChainAliasCallback,
+                XmppConnectionService.OnAccountCreated,
+                AccountAdapter.OnTglAccountState {
 
     private final String STATE_SELECTED_ACCOUNT = "selected_account";
 
@@ -50,7 +55,6 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
     protected Jid selectedAccountJid = null;
 
     protected final List<Account> accountList = new ArrayList<>();
-    protected ListView accountListView;
     protected AccountAdapter mAccountAdapter;
     protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
 
@@ -67,7 +71,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
             accountList.clear();
             accountList.addAll(xmppConnectionService.getAccounts());
         }
-        ActionBar actionBar = getSupportActionBar();
+        final ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {
             actionBar.setHomeButtonEnabled(this.accountList.size() > 0);
             actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0);
@@ -81,8 +85,11 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 
         super.onCreate(savedInstanceState);
 
-        setContentView(R.layout.activity_manage_accounts);
-        setSupportActionBar(findViewById(R.id.toolbar));
+        ActivityManageAccountsBinding binding =
+                DataBindingUtil.setContentView(this, R.layout.activity_manage_accounts);
+
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+        setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
         if (savedInstanceState != null) {
             String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT);
@@ -95,26 +102,19 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
             }
         }
 
-        accountListView = findViewById(R.id.account_list);
         this.mAccountAdapter = new AccountAdapter(this, accountList);
-        accountListView.setAdapter(this.mAccountAdapter);
-        accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position)));
-        registerForContextMenu(accountListView);
+        binding.accountList.setAdapter(this.mAccountAdapter);
+        binding.accountList.setOnItemClickListener(
+                (arg0, view, position, arg3) -> switchToAccount(accountList.get(position)));
+        registerForContextMenu(binding.accountList);
     }
 
-    @Override
-    protected void onStart() {
-        super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
-    }
 
     @Override
-    public void onSaveInstanceState(final Bundle savedInstanceState) {
+    public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
         if (selectedAccount != null) {
-            savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString());
+            savedInstanceState.putString(
+                    STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString());
         }
         super.onSaveInstanceState(savedInstanceState);
     }
@@ -122,8 +122,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
     @Override
     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
         super.onCreateContextMenu(menu, v, menuInfo);
-        ManageAccountActivity.this.getMenuInflater().inflate(
-                R.menu.manageaccounts_context, menu);
+        ManageAccountActivity.this.getMenuInflater().inflate(R.menu.manageaccounts_context, menu);
         AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
         this.selectedAccount = accountList.get(acmi.position);
         if (this.selectedAccount.isEnabled()) {
@@ -138,15 +137,16 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         if (selectedAccountJid != null) {
             this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
         }
         refreshUiReal();
         if (this.mPostponedActivityResult != null) {
-            this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
+            this.onActivityResult(
+                    mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
         }
-        if (Config.X509_VERIFICATION && this.accountList.size() == 0) {
+        if (Config.X509_VERIFICATION && this.accountList.isEmpty()) {
             if (mInvokedAddAccount.compareAndSet(false, true)) {
                 addAccountFromKey();
             }
@@ -233,9 +233,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
         return super.onOptionsItemSelected(item);
     }
 
-
     @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         if (grantResults.length > 0) {
             if (allGranted(grantResults)) {
@@ -258,13 +258,14 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
     @Override
     public boolean onNavigateUp() {
         if (xmppConnectionService.getConversations().size() == 0) {
-            Intent contactsIntent = new Intent(this,
-                    StartConversationActivity.class);
+            Intent contactsIntent = new Intent(this, StartConversationActivity.class);
             contactsIntent.setFlags(
                     // if activity exists in stack, pop the stack and go back to it
-                    Intent.FLAG_ACTIVITY_CLEAR_TOP |
+                    Intent.FLAG_ACTIVITY_CLEAR_TOP
+                            |
                             // otherwise, make a new task for it
-                            Intent.FLAG_ACTIVITY_NEW_TASK |
+                            Intent.FLAG_ACTIVITY_NEW_TASK
+                            |
                             // don't use the new activity animation; finish
                             // animation runs instead
                             Intent.FLAG_ACTIVITY_NO_ANIMATION);
@@ -286,16 +287,17 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
     }
 
     private void addAccountFromKey() {
+        Log.d(Config.LOGTAG, "add account from key");
         try {
             KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
-        } catch (ActivityNotFoundException e) {
-            Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
+        } catch (final ActivityNotFoundException e) {
+            Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG)
+                    .show();
         }
     }
 
     private void publishAvatar(Account account) {
-        Intent intent = new Intent(getApplicationContext(),
-                PublishProfilePictureActivity.class);
+        Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
         intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
         startActivity(intent);
     }
@@ -377,7 +379,6 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
         }
     }
 
-
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
@@ -385,7 +386,8 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
             if (xmppConnectionServiceBound) {
                 if (requestCode == REQUEST_CHOOSE_PGP_ID) {
                     if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
-                        selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
+                        selectedAccount.setPgpSignId(
+                                data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
                         announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
                     } else {
                         choosePgpSignId(selectedAccount);
@@ -402,9 +404,17 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 
     @Override
     public void alias(final String alias) {
-        if (alias != null) {
-            xmppConnectionService.createAccountFromKey(alias, this);
-        }
+        if (Strings.isNullOrEmpty(alias)) {
+            runOnUiThread(
+                    () ->
+                            Toast.makeText(
+                                            this,
+                                            R.string.no_certificate_selected,
+                                            Toast.LENGTH_LONG)
+                                    .show());
+            return;
+        }
+        xmppConnectionService.createAccountFromKey(alias, this);
     }
 
     @Override
@@ -417,6 +427,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 
     @Override
     public void informUser(final int r) {
-        runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
+        runOnUiThread(
+                () -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
     }
 }

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

@@ -22,19 +22,10 @@ public class PickServerActivity extends XmppActivity {
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
 
     }
 
-    @Override
-    public void onStart() {
-        super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
-    }
-
 
     @Override
     public boolean onOptionsItemSelected(final MenuItem item) {
@@ -53,7 +44,8 @@ public class PickServerActivity extends XmppActivity {
     }
 
     @Override
-    public void onNewIntent(Intent intent) {
+    public void onNewIntent(final Intent intent) {
+        super.onNewIntent(intent);
         if (intent != null) {
             setIntent(intent);
         }
@@ -66,6 +58,7 @@ public class PickServerActivity extends XmppActivity {
         }
         super.onCreate(savedInstanceState);
         ActivityPickServerBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_pick_server);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
         binding.useCim.setOnClickListener(v -> {
@@ -81,7 +74,7 @@ public class PickServerActivity extends XmppActivity {
             if (accounts.size() == 1) {
                 intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
                 intent.putExtra("init", true);
-            } else if (accounts.size() >= 1) {
+            } else if (!accounts.isEmpty()) {
                 intent = new Intent(this, ManageAccountActivity.class);
             }
             addInviteUri(intent);

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

@@ -57,16 +57,7 @@ public class ShareViaAccountActivity extends XmppActivity {
     }
 
     @Override
-    protected void onStart() {
-        super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
-    }
-
-    @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         final int numAccounts = xmppConnectionService.getAccounts().size();
 
         if (numAccounts == 1) {

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

@@ -34,7 +34,10 @@ import eu.siacs.conversations.xmpp.Jid;
 import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
 import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
 
-public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback {
+import com.google.common.base.Strings;
+
+public class WelcomeActivity extends XmppActivity
+        implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback {
 
     private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
 
@@ -66,7 +69,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
         final Intent intent;
         if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
             intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
-        } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
+        } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER)
+                && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
             intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
             intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
         } else {
@@ -81,22 +85,14 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
     }
 
     @Override
-    protected void refreshUiReal() {
-
-    }
+    protected void refreshUiReal() {}
 
     @Override
-    void onBackendConnected() {
-
-    }
+    protected void onBackendConnected() {}
 
     @Override
     public void onStart() {
         super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
         new InstallReferrerUtils(this);
     }
 
@@ -119,42 +115,44 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         }
         super.onCreate(savedInstanceState);
-        ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome);
+        ActivityWelcomeBinding binding =
+                DataBindingUtil.setContentView(this, R.layout.activity_welcome);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar(), false);
-        binding.registerNewAccount.setOnClickListener(v -> {
-            final Intent intent = new Intent(this, PickServerActivity.class);
-            addInviteUri(intent);
-            startActivity(intent);
-        });
-        binding.useExisting.setOnClickListener(v -> {
-            final List<Account> accounts = xmppConnectionService.getAccounts();
-            Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
-            intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false);
-            if (accounts.size() == 1) {
-                intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
-                intent.putExtra("init", true);
-            } else if (accounts.size() >= 1) {
-                intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
-            }
-            addInviteUri(intent);
-            startActivity(intent);
-        });
-
+        setTitle(null);
+        binding.registerNewAccount.setOnClickListener(
+                v -> {
+                    final Intent intent = new Intent(this, PickServerActivity.class);
+                    addInviteUri(intent);
+                    startActivity(intent);
+                });
+        binding.useExisting.setOnClickListener(
+                v -> {
+                    final List<Account> accounts = xmppConnectionService.getAccounts();
+                    Intent intent = new Intent(this, EditAccountActivity.class);
+                    intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false);
+                    if (accounts.size() == 1) {
+                        intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
+                        intent.putExtra("init", true);
+                    } else if (!accounts.isEmpty()) {
+                        intent = new Intent(this, ManageAccountActivity.class);
+                    }
+                    addInviteUri(intent);
+                    startActivity(intent);
+                });
     }
 
     @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
+    public boolean onCreateOptionsMenu(final Menu menu) {
         getMenuInflater().inflate(R.menu.welcome_menu, menu);
         final MenuItem scan = menu.findItem(R.id.action_scan_qr_code);
         scan.setVisible(Compatibility.hasFeatureCamera(this));
         return super.onCreateOptionsMenu(menu);
     }
 
-
-
     @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
+    public boolean onOptionsItemSelected(final MenuItem item) {
         switch (item.getItemId()) {
             case R.id.action_import_backup:
                 if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
@@ -174,16 +172,25 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
     private void addAccountFromKey() {
         try {
             KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
-        } catch (ActivityNotFoundException e) {
-            Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
+        } catch (final ActivityNotFoundException e) {
+            Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG)
+                    .show();
         }
     }
 
     @Override
     public void alias(final String alias) {
-        if (alias != null) {
-            xmppConnectionService.createAccountFromKey(alias, this);
+        if (Strings.isNullOrEmpty(alias)) {
+            runOnUiThread(
+                    () ->
+                            Toast.makeText(
+                                            this,
+                                            R.string.no_certificate_selected,
+                                            Toast.LENGTH_LONG)
+                                    .show());
+            return;
         }
+        xmppConnectionService.createAccountFromKey(alias, this);
     }
 
     @Override
@@ -201,7 +208,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
     }
 
     @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
         if (grantResults.length > 0) {
@@ -211,7 +219,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
                         startActivity(new Intent(this, ImportBackupActivity.class));
                         break;
                 }
-            } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+            } else if (Arrays.asList(permissions)
+                    .contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                 Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
             }
         }
@@ -232,5 +241,4 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
             to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString());
         }
     }
-
 }

src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java 🔗

@@ -22,7 +22,7 @@ import java.util.List;
 import java.util.concurrent.RejectedExecutionException;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.AccountRowBinding;
+import eu.siacs.conversations.databinding.ItemAccountBinding;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.ImportBackupService;
 import eu.siacs.conversations.utils.BackupFileHeader;
@@ -39,7 +39,7 @@ public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.Ba
     @NonNull
     @Override
     public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
-        return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false));
+        return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_account, viewGroup, false));
     }
 
     @Override
@@ -73,9 +73,9 @@ public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.Ba
     }
 
     static class BackupFileViewHolder extends RecyclerView.ViewHolder {
-        private final AccountRowBinding binding;
+        private final ItemAccountBinding binding;
 
-        BackupFileViewHolder(AccountRowBinding binding) {
+        BackupFileViewHolder(ItemAccountBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
         }
@@ -91,7 +91,7 @@ public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.Ba
         private Jid jid  = null;
         private final int size;
 
-        BitmapWorkerTask(ImageView imageView) {
+        BitmapWorkerTask(final ImageView imageView) {
             imageViewReference = new WeakReference<>(imageView);
             DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics();
 		this.size = ((int) (48 * metrics.density));
@@ -146,8 +146,7 @@ public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.Ba
     private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
         if (imageView != null) {
             final Drawable drawable = imageView.getDrawable();
-            if (drawable instanceof AsyncDrawable) {
-                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+            if (drawable instanceof AsyncDrawable asyncDrawable) {
                 return asyncDrawable.getBitmapWorkerTask();
             }
         }

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

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="512"
+    android:viewportHeight="512">
+    <group android:translateX="5.31">
+        <path
+            android:fillColor="@android:color/white"
+            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/layout/activity_easy_invite.xml 🔗

@@ -1,17 +1,23 @@
 <?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">
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
 
     <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"
-        android:background="?attr/color_background_primary"
         android:orientation="vertical">
 
-        <include
-            android:id="@+id/toolbar"
-            layout="@layout/toolbar" />
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
 
         <LinearLayout
             android:id="@+id/in_progress"
@@ -41,7 +47,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:text="@string/tap_share_button_send_invite"
-                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                android:textAppearance="?textAppearanceBodyMedium" />
 
             <TextView
                 android:id="@+id/scan_the_code"
@@ -50,7 +56,7 @@
                 android:layout_below="@+id/tap_to_share"
                 android:layout_marginTop="24sp"
                 android:text="@string/if_contact_is_nearby_use_qr"
-                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                android:textAppearance="?textAppearanceBodyMedium" />
 
             <ImageView
                 android:id="@+id/qr_code"
@@ -59,23 +65,19 @@
                 android:layout_above="@+id/share_button"
                 android:layout_below="@id/scan_the_code"
                 android:layout_alignParentStart="true"
-                android:layout_alignParentRight="true"
+                android:layout_alignParentEnd="true"
                 android:layout_centerHorizontal="true"
                 android:layout_margin="24sp"
                 android:scaleType="fitCenter" />
 
             <Button
                 android:id="@+id/share_button"
-                style="@style/Widget.Conversations.Button.Borderless"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_alignParentBottom="true"
-                android:minWidth="0dp"
-                android:paddingLeft="16dp"
-                android:paddingRight="16dp"
-                android:text="@string/share"
                 android:layout_centerHorizontal="true"
-                android:textColor="?attr/colorAccent" />
+                android:layout_marginHorizontal="16dp"
+                android:text="@string/share" />
 
         </RelativeLayout>
 

src/conversations/res/layout/activity_import_backup.xml 🔗

@@ -2,22 +2,30 @@
 <layout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto">
 
-
     <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"
-        android:background="?attr/color_background_primary"
         android:orientation="vertical">
 
-        <include
-            android:id="@+id/toolbar"
-            layout="@layout/toolbar" />
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
+
         <LinearLayout
-            android:visibility="gone"
             android:id="@+id/in_progress"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:gravity="center">
+            android:gravity="center"
+            android:visibility="gone">
+
             <ProgressBar
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
@@ -25,18 +33,15 @@
         </LinearLayout>
 
 
-
         <androidx.coordinatorlayout.widget.CoordinatorLayout
             android:id="@+id/coordinator"
             android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="?attr/color_background_primary">
+            android:layout_height="match_parent">
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/list"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
-                android:background="?attr/color_background_primary"
                 android:orientation="vertical"
                 app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
         </androidx.coordinatorlayout.widget.CoordinatorLayout>

src/conversations/res/layout/magic_create.xml → src/conversations/res/layout/activity_magic_create.xml 🔗

@@ -7,7 +7,18 @@
         android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <include layout="@layout/toolbar" android:id="@+id/toolbar"/>
+        <com.google.android.material.appbar.AppBarLayout
+            android:id="@+id/app_bar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
 
         <ScrollView
             android:layout_width="match_parent"
@@ -16,15 +27,13 @@
 
             <RelativeLayout
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/color_background_primary">
+                android:layout_height="wrap_content">
 
                 <LinearLayout
                     android:id="@+id/linearLayout"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true"
                     android:layout_alignParentBottom="true"
                     android:minHeight="256dp"
                     android:orientation="vertical"
@@ -42,50 +51,53 @@
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:text="@string/pick_your_username"
-                        android:textAppearance="@style/TextAppearance.Conversations.Title" />
+                        android:textAppearance="?textAppearanceTitleLarge" />
 
                     <TextView
                         android:id="@+id/instructions"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_marginTop="8dp"
+                        android:layout_marginVertical="8dp"
                         android:text="@string/magic_create_text"
-                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                        android:textAppearance="?textAppearanceBodyMedium" />
 
-                    <EditText
-                        android:id="@+id/username"
+                    <com.google.android.material.textfield.TextInputLayout
+                        android:id="@+id/username_layout"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="center_horizontal"
-                        android:hint="@string/username_hint"
-                        android:textColor="?attr/edit_text_color"
-                        android:inputType="textNoSuggestions" />
+                        android:hint="@string/username_hint">
+
+                        <com.google.android.material.textfield.TextInputEditText
+                            android:id="@+id/username"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:inputType="textNoSuggestions" />
+                    </com.google.android.material.textfield.TextInputLayout>
 
                     <TextView
                         android:id="@+id/full_jid"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_marginTop="8dp"
+                        android:layout_marginVertical="8dp"
                         android:text="@string/your_full_jid_will_be"
-                        android:textAppearance="@style/TextAppearance.Conversations.Caption"
+                        android:textAppearance="?textAppearanceLabelSmall"
                         android:visibility="invisible" />
 
                     <Button
                         android:id="@+id/create_account"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TonalButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/next"
-                        android:textColor="?colorAccent" />
+                        android:layout_gravity="end"
+                        android:text="@string/next" />
                 </LinearLayout>
 
                 <RelativeLayout
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:layout_above="@+id/linearLayout"
-                    android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true">
+                    android:layout_alignParentStart="true">
 
                     <ImageView
                         android:layout_width="wrap_content"

src/conversations/res/layout/activity_pick_server.xml 🔗

@@ -7,7 +7,17 @@
         android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <include android:id="@+id/toolbar" layout="@layout/toolbar" />
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
 
         <ScrollView
             android:layout_width="match_parent"
@@ -16,15 +26,13 @@
 
             <RelativeLayout
                 android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:background="?attr/color_background_primary">
+                android:layout_height="wrap_content">
 
                 <LinearLayout
                     android:id="@+id/linearLayout"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true"
                     android:layout_alignParentBottom="true"
                     android:minHeight="256dp"
                     android:orientation="vertical"
@@ -41,40 +49,38 @@
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:text="@string/pick_a_server"
-                        android:textAppearance="@style/TextAppearance.Conversations.Title" />
+                        android:textAppearance="?textAppearanceTitleLarge" />
 
                     <TextView
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:layout_marginTop="8dp"
+                        android:layout_marginBottom="16dp"
                         android:text="@string/server_select_text"
-                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                        android:textAppearance="?textAppearanceBodyMedium" />
 
                     <Button
                         android:id="@+id/use_cim"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TonalButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/use_conversations.im"
-                        android:textColor="?colorAccent" />
+                        android:layout_gravity="end"
+                        android:text="@string/use_conversations.im" />
 
                     <Button
                         android:id="@+id/use_own_provider"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TextButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/use_own_provider"
-                        android:textColor="?android:textColorSecondary" />
+                        android:layout_gravity="end"
+                        android:text="@string/use_own_provider" />
                 </LinearLayout>
 
                 <RelativeLayout
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:layout_above="@+id/linearLayout"
-                    android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true">
+                    android:layout_alignParentStart="true">
 
                     <ImageView
                         android:layout_width="wrap_content"

src/conversations/res/layout/activity_welcome.xml 🔗

@@ -6,9 +6,18 @@
         android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <include
-            android:id="@+id/toolbar"
-            layout="@layout/toolbar" />
+        <com.google.android.material.appbar.AppBarLayout
+            android:id="@+id/app_bar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize" />
+
+        </com.google.android.material.appbar.AppBarLayout>
 
         <ScrollView
             android:layout_width="match_parent"
@@ -17,15 +26,13 @@
 
             <RelativeLayout
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/color_background_primary">
+                android:layout_height="wrap_content">
 
                 <LinearLayout
                     android:id="@+id/linearLayout"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true"
                     android:layout_alignParentBottom="true"
                     android:minHeight="256dp"
                     android:orientation="vertical"
@@ -42,40 +49,38 @@
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:text="@string/welcome_header"
-                        android:textAppearance="@style/TextAppearance.Conversations.Title" />
+                        android:textAppearance="?textAppearanceTitleLarge" />
 
                     <TextView
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:layout_marginTop="8dp"
+                        android:layout_marginBottom="16dp"
                         android:text="@string/do_you_have_an_account"
-                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+                        android:textAppearance="?textAppearanceBodyMedium" />
 
                     <Button
                         android:id="@+id/register_new_account"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TonalButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/create_new_account"
-                        android:textColor="?colorAccent" />
+                        android:layout_gravity="end"
+                        android:text="@string/create_new_account" />
 
                     <Button
                         android:id="@+id/use_existing"
-                        style="@style/Widget.Conversations.Button.Borderless"
+                        style="@style/Widget.Material3.Button.TextButton"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_gravity="right"
-                        android:text="@string/i_already_have_an_account"
-                        android:textColor="?android:textColorSecondary" />
+                        android:layout_gravity="end"
+                        android:text="@string/i_already_have_an_account" />
                 </LinearLayout>
 
                 <RelativeLayout
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:layout_above="@+id/linearLayout"
-                    android:layout_alignParentStart="true"
-                    android:layout_alignParentLeft="true">
+                    android:layout_alignParentStart="true">
 
                     <ImageView
                         android:layout_width="wrap_content"

src/conversations/res/layout/dialog_enter_password.xml 🔗

@@ -1,54 +1,53 @@
 <?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:app="http://schemas.android.com/apk/res-auto">
 
-    <LinearLayout
+    <ScrollView
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:padding="?dialogPreferredPadding">
+        android:layout_height="match_parent">
 
-        <TextView
-            android:id="@+id/explain"
+        <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/enter_password_to_restore"
-            android:textAppearance="@style/TextAppearance.Conversations.Body2"/>
+            android:orientation="vertical"
+            android:padding="?dialogPreferredPadding">
+            
+            <TextView
+                android:id="@+id/explain"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/enter_password_to_restore"
+                android:textAppearance="?textAppearanceBodyMedium" />
 
-        <TextView
-            android:layout_marginTop="?TextSizeBody1"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/restore_warning"
-            android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="18sp"
+                android:text="@string/restore_warning"
+                android:textAppearance="?textAppearanceBodyMedium" />
 
-        <TextView
-            android:layout_marginTop="?TextSizeBody1"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/restore_warning_continued"
-            android:textAppearance="@style/TextAppearance.Conversations.Subhead.Bold"/>
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="18sp"
+                android:text="@string/restore_warning_continued"
+                android:textAppearance="?textAppearanceBodyMedium" />
 
-        <com.google.android.material.textfield.TextInputLayout
-            android:id="@+id/account_password_layout"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="8dp"
-            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">
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/account_password_layout"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                app:endIconMode="password_toggle">
 
-        <eu.siacs.conversations.ui.widget.TextInputEditText
-            android:id="@+id/account_password"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:hint="@string/password"
-            android:inputType="textPassword"
-            android:textColor="?attr/edit_text_color"
-            style="@style/Widget.Conversations.EditText"/>
+                <eu.siacs.conversations.ui.widget.TextInputEditText
+                    android:id="@+id/account_password"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="@string/password"
+                    android:inputType="textPassword" />
 
-        </com.google.android.material.textfield.TextInputLayout>
-    </LinearLayout>
+            </com.google.android.material.textfield.TextInputLayout>
+        </LinearLayout>
+    </ScrollView>
 </layout>

src/conversations/res/menu/manageaccounts.xml 🔗

@@ -1,32 +1,31 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-	  xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
-	<item
-		android:id="@+id/action_add_account"
-		android:icon="?attr/icon_add_person"
-		app:showAsAction="always"
-		android:title="@string/action_add_account"/>
-	<item
-		android:id="@+id/action_import_backup"
-		app:showAsAction="never"
-		android:title="@string/restore_backup"/>
-	<item
-		android:id="@+id/action_add_account_with_cert"
-		app:showAsAction="never"
-		android:icon="?attr/icon_add_person"
-		android:title="@string/action_add_account_with_certificate"
-		android:visible="true"/>
-	<item
-		android:id="@+id/action_enable_all"
-		android:title="@string/enable_all_accounts"/>
-	<item
-		android:id="@+id/action_disable_all"
-		android:title="@string/disable_all_accounts"/>
-	<item
-		android:id="@+id/action_settings"
-		android:orderInCategory="100"
-		app:showAsAction="never"
-		android:title="@string/action_settings"/>
+    <item
+        android:id="@+id/action_add_account"
+        android:icon="@drawable/ic_person_add_24dp"
+        android:title="@string/action_add_account"
+        app:showAsAction="ifRoom" />
+    <item
+        android:id="@+id/action_import_backup"
+        android:title="@string/restore_backup"
+        app:showAsAction="never" />
+    <item
+        android:id="@+id/action_add_account_with_cert"
+        android:title="@string/action_add_account_with_certificate"
+        android:visible="true"
+        app:showAsAction="never" />
+    <item
+        android:id="@+id/action_enable_all"
+        android:title="@string/enable_all_accounts" />
+    <item
+        android:id="@+id/action_disable_all"
+        android:title="@string/disable_all_accounts" />
+    <item
+        android:id="@+id/action_settings"
+        android:orderInCategory="100"
+        android:title="@string/action_settings"
+        app:showAsAction="never" />
 
 </menu>

src/conversations/res/menu/welcome_menu.xml 🔗

@@ -3,7 +3,7 @@
 
     <item
         android:id="@+id/action_scan_qr_code"
-        android:icon="?attr/icon_scan_qr_code"
+        android:icon="@drawable/ic_qr_code_scanner_24dp"
         android:orderInCategory="10"
         android:title="@string/scan_qr_code"
         android:visible="@bool/show_qr_code_scan"

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

@@ -5,7 +5,7 @@
     <string name="create_new_account">أنشئ حسابًا جديدًا</string>
     <string name="do_you_have_an_account">هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق Conversations سابقا. أو يمكنك صنع حساب XMPP جديد الآن.
 \nملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات XMPP.</string>
-    <string name="server_select_text">XMPP هو مزود مستقل لشبكة المراسلة الفورية. يمكنك استخدام هذا العميل مع أي خادم XMPP تختاره.
+    <string name="server_select_text">XMPP هو مزود مستقل لشبكة المراسلة الفورية. يمكنك استخدام هذا التطبيق مع أي خادم XMPP تختاره.
 \nولكن من أجل راحتك ، فقد جعلنا من السهل إنشاء حساب على موقع chat. مزود مناسب بشكل خاص للاستخدام مع المحادثات.</string>
     <string name="magic_create_text_on_x">لقد تمت دعوتك إلى%1$s. سنوجهك خلال عملية إنشاء حساب.
 \nعند اختيار%1$s كموفر ، ستتمكن من التواصل مع مستخدمي مقدمي الخدمات الآخرين من خلال منحهم عنوان XMPP الكامل الخاص بك.</string>

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

@@ -5,7 +5,8 @@
     <string name="create_new_account">Създаване не нов профил</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="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>

src/conversations/res/values-da-rDK/strings.xml 🔗

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Brug conversations.im</string>
     <string name="create_new_account">Opret ny konto</string>
     <string name="do_you_have_an_account">Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti.</string>
-    <string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im; en udbyder, der er specielt velegnet til brug med Conversations.</string>
+    <string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.
+\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im; en udbyder, der er specielt velegnet til brug med Conversations.</string>
     <string name="magic_create_text_on_x">Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
     <string name="magic_create_text_fixed">Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
     <string name="your_server_invitation">Din server invitation</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Benutze conversations.im</string>
     <string name="create_new_account">Neues Konto erstellen</string>
     <string name="do_you_have_an_account">Hast du bereits ein XMPP-Konto? Dies kann der Fall sein, wenn du bereits einen anderen XMPP-Client verwendest oder bereits Conversations verwendet hast. Wenn nicht, kannst du jetzt ein neues XMPP-Konto erstellen.\nTipp: Einige E-Mail-Anbieter bieten auch XMPP-Konten an.</string>
-    <string name="server_select_text">XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen.\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist.</string>
+    <string name="server_select_text">XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diese App mit jedem beliebigen XMPP-Server nutzen.
+\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist.</string>
     <string name="magic_create_text_on_x">Du wurdest zu %1$s eingeladen. Wir führen dich durch den Prozess der Kontoerstellung.\nWenn du %1$s als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst.</string>
     <string name="magic_create_text_fixed">Du wurdest zu %1$seingeladen. Ein Benutzername ist bereits für dich ausgewählt worden. Wir führen dich durch den Prozess der Kontoerstellung.\nDu kannst mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst.</string>
     <string name="your_server_invitation">Deine Einladung für den Server</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Χρήση του conversations.im</string>
     <string name="create_new_account">Δημιουργία νέου λογαριασμού</string>
     <string name="do_you_have_an_account">Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP.</string>
-    <string name="server_select_text">Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations.</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>

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

@@ -4,8 +4,8 @@
     <string name="use_conversations.im">Usa conversations.im</string>
     <string name="create_new_account">Crear nueva cuenta</string>
     <string name="do_you_have_an_account">¿Ya tienes una cuenta XMPP? Este puede ser el caso si ya estás usando un cliente XMPP diferente o has usado Conversations anteriormente. Si no es así, puedes crear una nueva cuenta XMPP ahora mismo.\nConsejo: Algunos proveedores de email también ofrecen una cuenta XMPP.</string>
-    <string name="server_select_text">XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas.
-\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im; un proveedor especializado para el uso con Conversations.</string>
+    <string name="server_select_text">XMPP es una red de mensajería instantánea independiente del proveedor. Puedes utilizar esta aplicación con cualquier servidor XMPP que elijas.
+\nSin embargo, para tu comodidad, te facilitamos la creación de una cuenta en conversations.im, un proveedor específicamente adaptado para su uso con Conversations.</string>
     <string name="magic_create_text_on_x">Has sido invitado a %1$s. Te guiaremos durante el proceso de creación de la cuenta.\nCuando selecciones %1$s como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. </string>
     <string name="magic_create_text_fixed">Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa. </string>
     <string name="your_server_invitation">Tu invitación al servidor</string>

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

@@ -4,5 +4,6 @@
     <string name="use_conversations.im">Erabili conversations.im</string>
     <string name="create_new_account">Kontu berria sortu</string>
     <string name="do_you_have_an_account">XMPP kontu bat badaukazu dagoeneko? Horrela izan daiteke beste XMPP aplikazio bat erabiltzen baduzu edo Conversations lehenago erabili baduzu. Bestela XMPP kontu berri bat sortu dezakezu oraintxe bertan.\nIradokizuna: email hornitzaile batzuek XMPP kontuak hornitzen dituzte ere.</string>
-    <string name="server_select_text">XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu.\nHala ere zure erosotasunerako conversations.im-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu.</string>
-    </resources>
+    <string name="server_select_text">XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu.
+\nHala ere zure erosotasunerako conversations.im-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu.</string>
+</resources>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Käytä conversations.im:ää</string>
     <string name="create_new_account">Luo uusi tili</string>
     <string name="do_you_have_an_account">Onko sinulla jo XMPP-tunnus? Jos käytät jo toista XMPP-sovellusta tai olet käyttänyt Conversationsia aiemmin, niin voi olla. Jos ei, voit tehdä uuden XMPP-tilin saman tien.\nVinkki: Jotkin sähköpostipalvelut tarjoavat myös XMPP-tilin.</string>
-    <string name="server_select_text">XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa.\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin.</string>
+    <string name="server_select_text">XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa.
+\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin.</string>
     <string name="magic_create_text_on_x">Sinut on kutsuttu %1$s:iin. Opastamme sinua tilin luomisen kanssa.\nValitessasi palvelimen %1$s palveluntarjoajaksesi voit jutella muiden palveluntajoajien käyttäjien kanssa kertomalla heille koko XMPP-osoitteesi.</string>
     <string name="magic_create_text_fixed">Sinut on kutsuttu palvelimelle %1$s. Käyttäjänimesi on valittu valmiiksi puolestasi. Opastamme sinua tilin luomisen kanssa.\nVoit jutella muiden palveluntarjoajien käyttäjien kanssa kertomalle heille koko XMPP-osoitteesi.</string>
     <string name="your_server_invitation">Kutsusi palvelimelle</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Utiliser conversations.im</string>
     <string name="create_new_account">Créer un nouveau compte</string>
     <string name="do_you_have_an_account">Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP.</string>
-    <string name="server_select_text">XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations.</string>
+    <string name="server_select_text">XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser cette application avec n’importe quel serveur XMPP de votre choix.
+\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations.</string>
     <string name="magic_create_text_on_x">Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète.</string>
     <string name="magic_create_text_fixed">Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète.</string>
     <string name="your_server_invitation">Votre invitation au serveur</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Utilizar conversations.im</string>
     <string name="create_new_account">Crear nova conta</string>
     <string name="do_you_have_an_account">Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP.</string>
-    <string name="server_select_text">XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im; un provedor especialmente axeitado para utilizar con Conversations.</string>
+    <string name="server_select_text">XMPP é unha rede de mensaxería independente-do-provedor. Podes utilizar esta app con calquera provedor XMPP da túa elección.
+\nEmporiso, pola tua comenencia, fixemos que fose doado crear unha conta en conversations.im; un provedor moi axeitado para utilizar con Conversations.</string>
     <string name="magic_create_text_on_x">Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo.</string>
     <string name="magic_create_text_fixed">Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo.</string>
     <string name="your_server_invitation">O convite do teu servidor</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Koristite conversations.im</string>
     <string name="create_new_account">Napravi novi račun</string>
     <string name="do_you_have_an_account">Već imate XMPP račun? To može biti slučaj ako već koristite drugi XMPP klijent ili ste prije koristili Razgovore. Ako niste, možete odmah stvoriti novi XMPP račun.\nSavjet: Neki pružatelji usluga e-pošte također nude XMPP račune.</string>
-    <string name="server_select_text">XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete.\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations.</string>
+    <string name="server_select_text">XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete.
+\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations.</string>
     <string name="magic_create_text_on_x">Pozvani ste na %1$s. Vodit ćemo vas kroz postupak kreiranja računa.\nPrilikom odabira  %1$s pružatelja moći ćete komunicirati s korisnicima drugih pružatelja dajući im svoju punu XMPP adresu.</string>
     <string name="magic_create_text_fixed">Pozvani ste na %1$s. Korisničko ime je već odabrano za vas. Vodit ćemo vas kroz postupak kreiranja računa.\nMoći ćete komunicirati s korisnicima drugih pružatelja tako da im date svoju punu XMPP adresu.</string>
     <string name="your_server_invitation">Vaša pozivnica za poslužitelj</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">A conversations.im használata</string>
     <string name="create_new_account">Új fiók létrehozása</string>
     <string name="do_you_have_an_account">Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat.</string>
-    <string name="server_select_text">Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve.</string>
+    <string name="server_select_text">Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.
+\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve.</string>
     <string name="magic_create_text_on_x">Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor képes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét.</string>
     <string name="magic_create_text_fixed">Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét.</string>
     <string name="your_server_invitation">Az Ön kiszolgálómeghívása</string>

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

@@ -5,7 +5,7 @@
     <string name="create_new_account">Crea un nuovo profilo</string>
     <string name="do_you_have_an_account">Hai già un profilo XMPP\? Può accadere se stai già usando un client XMPP diverso o hai già usato prima Conversations. In caso negativo, puoi creare un profilo XMPP adesso.
 \nNota: alcuni fornitori di email offrono anche account XMPP.</string>
-    <string name="server_select_text">XMPP è una rete di messaggistica istantanea indipendente dal fornitore. Puoi usare questo client con qualsiasi server XMPP.
+    <string name="server_select_text">XMPP è una rete di messaggistica istantanea indipendente dal fornitore. Puoi usare questa app con qualsiasi server XMPP.
 \nTuttavia, per comodità, puoi creare facilmente un account su conversations.im; un fornitore pensato apposta per essere usato con Conversations.</string>
     <string name="magic_create_text_on_x">Hai ricevuto un invito per %1$s. Ti guideremo nel procedimento per creare un profilo.\nQuando scegli %1$s come fornitore sarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
     <string name="magic_create_text_fixed">Hai ricevuto un invito per %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un profilo.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">conversations.im を利用する</string>
     <string name="create_new_account">新規アカウントを作成</string>
     <string name="do_you_have_an_account">XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。</string>
-    <string name="server_select_text">XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im で簡単にアカウントを作成することもできます。</string>
+    <string name="server_select_text">XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このアプリを使用することができます。
+\nよろしければ、 Conversations に最適化されたプロバイダー 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="your_server_invitation">サーバーの招待</string>

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

@@ -4,12 +4,14 @@
     <string name="use_conversations.im">Conversations.im gebruiken</string>
     <string name="create_new_account">Nieuwe account registreren</string>
     <string name="do_you_have_an_account">Heb je al een XMPP-account? Als je al een andere XMPP-cliënt gebruikt, of Conversations vroeger al eens hebt gebruikt, is dit waarschijnlijk het geval. Zo niet, kan je nu een nieuwe XMPP-account aanmaken.\nTip: sommige e-mailproviders bieden ook XMPP-accounts aan.</string>
-    <string name="server_select_text">XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server.\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im; een provider speciaal geschikt voor Conversations.</string>
+    <string name="server_select_text">XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze applicatie gebruiken met eender welke XMPP-server.
+\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im; een provider speciaal geschikt voor Conversations.</string>
     <string name="magic_create_text_on_x">Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven.</string>
     <string name="magic_create_text_fixed">Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven.</string>
     <string name="your_server_invitation">Je server uitnodiging</string>
-    <string name="tap_share_button_send_invite">Tik op de delen knop om een uitnodiging te versturen naar %1$s</string>
+    <string name="tap_share_button_send_invite">Tik op de delen knop om een uitnodiging te versturen naar %1$s.</string>
     <string name="if_contact_is_nearby_use_qr">Als je contactpersoon in de buurt is, kan deze ook onderstaande code scannen om de uitnodiging te aanvaarden.</string>
-    <string name="share_invite_with">Deel de uitnodiging met ...</string>
+    <string name="share_invite_with">Deel de uitnodiging met…</string>
     <string name="easy_invite_share_text">Vergezel %1$s en chat met mij: %2$s</string>
+    <string name="improperly_formatted_provisioning">Onjuist geformatteerde provisioningcode</string>
 </resources>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Użyj conversations.im</string>
     <string name="create_new_account">Utwórz nowe konto</string>
     <string name="do_you_have_an_account">Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP.</string>
-    <string name="server_select_text">XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations.</string>
+    <string name="server_select_text">XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tej aplikacji z dowolnym serwerem XMPP.
+\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations.</string>
     <string name="magic_create_text_on_x">Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP.</string>
     <string name="magic_create_text_fixed">Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.</string>
     <string name="your_server_invitation">Zaproszenie twojego serwera</string>

src/conversations/res/values-pt-rBR/strings.xml 🔗

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Usar o conversations.im</string>
     <string name="create_new_account">Criar uma nova conta</string>
     <string name="do_you_have_an_account">Você já possui uma conta XMPP? Esse pode ser o seu caso caso já esteja usando um outro cliente XMPP ou tenha usado o Conversations antes. Caso contrário, você pode criar uma nova conta XMPP agora.\nDica: alguns provedores de e-mail também fornecem contas XMPP.</string>
-    <string name="server_select_text">O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations.</string>
+    <string name="server_select_text">O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.
+\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations.</string>
     <string name="magic_create_text_on_x">Você foi convidado para %1$s. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nAo escolher %1$s como um provedor você conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
     <string name="magic_create_text_fixed">Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
     <string name="your_server_invitation">Seu convite do servidor</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Folosește conversations.im</string>
     <string name="create_new_account">Creează un cont nou</string>
     <string name="do_you_have_an_account">Aveți deja un cont XMPP? S-ar putea să fie așa dacă deja utilizați un alt client XMPP sau dacă ați folosit Conversations în trecut. Dacă nu, puteți crea un cont nou XMPP chiar acum.\nIdee: Unii furnizori de e-mail oferă de asemenea și conturi XMPP.</string>
-    <string name="server_select_text">XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți.\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im; un furnizor potrivit pentru utilizarea cu aplicația Conversations.</string>
+    <string name="server_select_text">XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați această aplicație cu orice server XMPP doriți.
+\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im; un furnizor potrivit pentru utilizarea cu aplicația Conversations.</string>
     <string name="magic_create_text_on_x">Ați fost invitați la %1$s. Vă vom ghida prin procesul de creare al unui cont.\nCând alegeți %1$s ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP.</string>
     <string name="magic_create_text_fixed">Ați fost invitați la %1$s. Un nume de utilizator a fost deja ales pentru dumneavoastră. Vă vom ghida prin procesul de creare al unui cont.\nVeți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP.</string>
     <string name="your_server_invitation">Invitația serverului dumneavoastră</string>

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

@@ -5,7 +5,8 @@
     <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 - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations.</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. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. 

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Použiť conversations.im</string>
     <string name="create_new_account">Vytvoriť nové konto</string>
     <string name="do_you_have_an_account">Máte už svoje XMPP konto? Môže to tak byť v prípade, že už používate iného klienta XMPP alebo ste predtým používali Conversations. Ak nie, môžete si vytvoriť nové XMPP konto práve teraz.\nHint: Niektorí poskytovatelia emailu zároveň poskytujú aj XMPP kontá.</string>
-    <string name="server_select_text">XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im; poskytovateľ špeciálne vhodný na používanie s Conversations.</string>
+    <string name="server_select_text">XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..
+\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im; poskytovateľ špeciálne vhodný na používanie s Conversations.</string>
     <string name="magic_create_text_on_x">Boli ste pozvaný do %1$s. Prevedieme vás procesom vytvorenia konta..\nPo výbere %1$s ako poskytovateľa, budete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
     <string name="magic_create_text_fixed">Boli ste pozvaný do %1$s . Užívateľské meno vám už bolo vopred vybrané. Prevedieme vás procesom vytvorenia konta..\nBudete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
     <string name="tap_share_button_send_invite">Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu.</string>

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

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="server_select_text">XMPP është një rrjet shkëmbimi mesazhesh të atypëratyshëm i pavarur nga shërbimet. Këtë klient mund ta përdorni me cilindo shërbyes XMPP që zgjidhni.
+    <string name="server_select_text">XMPP është një rrjet shkëmbimi mesazhesh të atypëratyshëm i pavarur nga shërbimet. Këtë aplikacion mund ta përdorni me cilindo shërbyes XMPP që zgjidhni.
 \nMegjithatë, për lehtësi, e kemi bërë të kollajshme të krijohet një llogari te conversations.im, një shërbim posaçërisht i përshtatshëm për përdorim me Conversations.</string>
     <string name="magic_create_text_on_x">Jeni ftuar te %1$s. Do t’ju udhëheqim përmes procesit të krijimit të një llogarie.
 \nKur zgjidhet %1$s si shërbim, do të jeni në gjendje të komunikoni me përdorues nga shërbime të tjera duke u dhënë adresën tuaj të plotë XMPP.</string>

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

@@ -4,6 +4,7 @@
     <string name="use_conversations.im">Користи conversations.im</string>
     <string name="create_new_account">Направи нови налог</string>
     <string name="do_you_have_an_account">Да ли већ имате ИксМПП налог? Извесно је да га имате ако користите неки ИксМПП клијент или сте раније користили Конверзацију. Ако немате, сада можете направити нови ИксМПП налог.\nСавет: неки поштански провајдери такође омогућавају и ИксМПП налоге.</string>
-    <string name="server_select_text">ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију</string>
+    <string name="server_select_text">ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.
+\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију</string>
     <string name="your_server_invitation">Ваша серверска позивница</string>
-    </resources>
+</resources>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">Użyj conversations.im</string>
     <string name="create_new_account">Stwōrz nowe kōnto</string>
     <string name="do_you_have_an_account">Mosz już kōnto XMPP? Tak może być, jeźli już używosz inkszego klijynta XMPP aboś używoł abo używała wcześnij  Conversations. Jak niy, to możesz stworzić teroz nowe kōnto XMPP.\nDorada: Niykerzi liferańcio emaili dowajōm tyż kōnta XMPP.</string>
-    <string name="server_select_text">XMPP to je nec wartkich wiadōmości niyzależny ôd liferanta. Możesz używać tego klijynta ze serwerym XMPP, jaki sie wybieresz.\nAle dlo twojij wygody ułacniyli my tworzynie kōnt na conversations.im; liferańcie ekstra dopasowanym do używanio ze Conversations.</string>
+    <string name="server_select_text">XMPP to je nec wartkich wiadōmości niyzależny ôd liferanta. Możesz używać tego klijynta ze serwerym XMPP, jaki sie wybieresz.
+\nAle dlo twojij wygody ułacniyli my tworzynie kōnt na conversations.im; liferańcie ekstra dopasowanym do używanio ze Conversations.</string>
     <string name="magic_create_text_on_x">Mosz zaproszynie na %1$s. Pokludzymy cie bez proces tworzynio kōnta.\nPo wybraniu %1$s za liferanta, poradzisz kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP.</string>
     <string name="magic_create_text_fixed">Mosz zaproszynie na %1$s. Miano ôd używocza już je do ciebie wybrane. Pokludzymy cie bez proces tworzynio kōnta.\nBydzie szło kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP.</string>
     <string name="your_server_invitation">Twoje zaproszynie na serwer</string>

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

@@ -4,7 +4,8 @@
     <string name="use_conversations.im">conversations.im kullan</string>
     <string name="create_new_account">Yeni hesap oluştur</string>
     <string name="do_you_have_an_account">Zaten bir XMPP hesabınız var mı? Bunun sebebi, zaten başka bir XMPP istemcisi kullanıyor oluşunuz veya Conversations\'ı önceden kullanmış olmanız olabilir. Eğer durum bu değilse şimdi yeni bir XMPP hesabı oluşturabilirsiniz.\nİpucu: Bağzı e-posta sağlayıcıları da XMPP hesapları kullanabilir.</string>
-    <string name="server_select_text">XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.\nAncak kullanım rahatlığı adına sizin için conversations.im; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık.</string>
+    <string name="server_select_text">XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.
+\nAncak kullanım rahatlığı adına sizin için conversations.im; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık.</string>
     <string name="magic_create_text_on_x">%1$s sağlayıcısına davet edildiniz. Sizi hesap oluşturulması konusunda yönlendireceğiz.\n%1$s bir sağlayıcı olark seçildiğinde, başka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz.</string>
     <string name="magic_create_text_fixed">%1$s sağlayıcısına davet edildiniz. Sizin için zaten bir kullanıcı adı seçildi. Sizi hesap oluşturulması konusunda yönlendireceğiz.\nBaşka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz.</string>
     <string name="your_server_invitation">Sunucu davetiyeniz</string>

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

@@ -5,7 +5,7 @@
     <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 服务器都可以使用此客户端。
+    <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>

src/conversations/res/values/colors-themed.xml 🔗

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="md_theme_light_primary">#006E1C</color>
+    <color name="md_theme_light_onPrimary">#FFFFFF</color>
+    <color name="md_theme_light_primaryContainer">#98F994</color>
+    <color name="md_theme_light_onPrimaryContainer">#002204</color>
+    <color name="md_theme_light_secondary">#52634F</color>
+    <color name="md_theme_light_onSecondary">#FFFFFF</color>
+    <color name="md_theme_light_secondaryContainer">#D5E8CF</color>
+    <color name="md_theme_light_onSecondaryContainer">#111F0F</color>
+    <color name="md_theme_light_tertiary">#38656A</color>
+    <color name="md_theme_light_onTertiary">#FFFFFF</color>
+    <color name="md_theme_light_tertiaryContainer">#BCEBF0</color>
+    <color name="md_theme_light_onTertiaryContainer">#002023</color>
+    <color name="md_theme_light_error">#BA1A1A</color>
+    <color name="md_theme_light_errorContainer">#FFDAD6</color>
+    <color name="md_theme_light_onError">#FFFFFF</color>
+    <color name="md_theme_light_onErrorContainer">#410002</color>
+    <color name="md_theme_light_background">#FCFDF6</color>
+    <color name="md_theme_light_onBackground">#1A1C19</color>
+    <color name="md_theme_light_surface">#FCFDF6</color>
+    <color name="md_theme_light_onSurface">#1A1C19</color>
+    <color name="md_theme_light_surfaceVariant">#DEE5D8</color>
+    <color name="md_theme_light_onSurfaceVariant">#424940</color>
+    <color name="md_theme_light_outline">#72796F</color>
+    <color name="md_theme_light_inverseOnSurface">#F0F1EB</color>
+    <color name="md_theme_light_inverseSurface">#2F312D</color>
+    <color name="md_theme_light_inversePrimary">#7DDC7A</color>
+    <color name="md_theme_light_shadow">#000000</color>
+    <color name="md_theme_light_surfaceTint">#006E1C</color>
+    <color name="md_theme_light_outlineVariant">#C2C9BD</color>
+    <color name="md_theme_light_scrim">#000000</color>
+    <color name="md_theme_dark_primary">#7DDC7A</color>
+    <color name="md_theme_dark_onPrimary">#00390A</color>
+    <color name="md_theme_dark_primaryContainer">#005313</color>
+    <color name="md_theme_dark_onPrimaryContainer">#98F994</color>
+    <color name="md_theme_dark_secondary">#BACCB3</color>
+    <color name="md_theme_dark_onSecondary">#253423</color>
+    <color name="md_theme_dark_secondaryContainer">#3B4B38</color>
+    <color name="md_theme_dark_onSecondaryContainer">#D5E8CF</color>
+    <color name="md_theme_dark_tertiary">#A0CFD4</color>
+    <color name="md_theme_dark_onTertiary">#00363B</color>
+    <color name="md_theme_dark_tertiaryContainer">#1F4D52</color>
+    <color name="md_theme_dark_onTertiaryContainer">#BCEBF0</color>
+    <color name="md_theme_dark_error">#FFB4AB</color>
+    <color name="md_theme_dark_errorContainer">#93000A</color>
+    <color name="md_theme_dark_onError">#690005</color>
+    <color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
+    <color name="md_theme_dark_background">#1A1C19</color>
+    <color name="md_theme_dark_onBackground">#E2E3DD</color>
+    <color name="md_theme_dark_surface">#1A1C19</color>
+    <color name="md_theme_dark_onSurface">#E2E3DD</color>
+    <color name="md_theme_dark_surfaceVariant">#424940</color>
+    <color name="md_theme_dark_onSurfaceVariant">#C2C9BD</color>
+    <color name="md_theme_dark_outline">#8C9388</color>
+    <color name="md_theme_dark_inverseOnSurface">#1A1C19</color>
+    <color name="md_theme_dark_inverseSurface">#E2E3DD</color>
+    <color name="md_theme_dark_inversePrimary">#006E1C</color>
+    <color name="md_theme_dark_shadow">#000000</color>
+    <color name="md_theme_dark_surfaceTint">#7DDC7A</color>
+    <color name="md_theme_dark_outlineVariant">#424940</color>
+    <color name="md_theme_dark_scrim">#000000</color>
+</resources>

src/free/AndroidManifest.xml 🔗

@@ -5,4 +5,9 @@
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.READ_PROFILE" />
+
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
 </manifest>

src/main/AndroidManifest.xml 🔗

@@ -5,9 +5,6 @@
     <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.READ_PHONE_STATE"
-        android:maxSdkVersion="22" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -50,6 +47,8 @@
     <!-- this foreground service type permission is exclusively used for import and export backup -->
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
 
+    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+
     <uses-feature
         android:name="android.hardware.camera"
         android:required="false" />
@@ -85,6 +84,7 @@
 
 
     <application
+        android:name=".Conversations"
         android:allowBackup="true"
         android:appCategory="social"
         android:dataExtractionRules="@xml/data_extraction_rules"
@@ -97,7 +97,7 @@
         android:networkSecurityConfig="@xml/network_security_configuration"
         android:preserveLegacyExternalStorage="true"
         android:requestLegacyExternalStorage="true"
-        android:theme="@style/ConversationsTheme"
+        android:theme="@style/Theme.Conversations3"
         tools:targetApi="tiramisu">
 
         <meta-data
@@ -133,11 +133,21 @@
             </intent-filter>
         </service>
 
+        <service
+            android:name=".services.CallIntegrationConnectionService"
+            android:exported="true"
+            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
+            <intent-filter>
+                <action android:name="android.telecom.ConnectionService" />
+            </intent-filter>
+        </service>
+
         <receiver
             android:name=".services.EventReceiver"
             android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
                 <action
                     android:name="android.net.conn.CONNECTIVITY_CHANGE"
                     tools:ignore="BatteryLife" />
@@ -175,7 +185,7 @@
             android:name=".ui.RecordingActivity"
             android:exported="false"
             android:configChanges="orientation|screenSize"
-            android:theme="@style/ConversationsTheme.Dialog" />
+            android:theme="@style/Theme.Conversations3.Dialog" />
         <activity
             android:name=".ui.ShowLocationActivity"
             android:label="@string/title_activity_show_location"
@@ -183,7 +193,7 @@
         <activity
             android:name=".ui.ConversationActivity"
             android:exported="true"
-            android:theme="@style/SplashTheme">
+            android:theme="@style/Theme.Conversations3.SplashScreen">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -201,7 +211,7 @@
             android:name=".ui.ScanActivity"
             android:exported="false"
             android:screenOrientation="portrait"
-            android:theme="@style/ConversationsTheme.FullScreen"
+            android:theme="@style/Theme.Conversations3.FullScreen"
             android:windowSoftInputMode="stateAlwaysHidden" />
         <activity
             android:name=".ui.UriHandlerActivity"
@@ -264,14 +274,14 @@
         <activity
             android:name=".ui.StartConversationActivity"
             android:exported="true"
-            android:label="@string/title_activity_start_conversation"
+            android:label="@string/title_activity_new_chat"
             android:launchMode="singleTop">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
             </intent-filter>
         </activity>
         <activity
-            android:name=".ui.SettingsActivity"
+            android:name=".ui.activity.SettingsActivity"
             android:exported="true"
             android:label="@string/title_activity_settings">
             <intent-filter>

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

@@ -0,0 +1,143 @@
+package eu.siacs.conversations;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+
+import androidx.annotation.BoolRes;
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceManager;
+
+import com.google.common.base.Strings;
+
+public class AppSettings {
+
+    public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
+    public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off";
+    public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent";
+    public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode";
+    public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence";
+    public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
+    public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
+    public static final String BROADCAST_LAST_ACTIVITY = "last_activity";
+    public static final String THEME = "theme";
+    public static final String DYNAMIC_COLORS = "dynamic_colors";
+    public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
+    public static final String OMEMO = "omemo";
+    public static final String ALLOW_SCREENSHOTS = "allow_screenshots";
+    public static final String RINGTONE = "call_ringtone";
+    public static final String BTBV = "btbv";
+
+    public static final String CONFIRM_MESSAGES = "confirm_messages";
+    public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction";
+
+    public static final String TRUST_SYSTEM_CA_STORE = "trust_system_ca_store";
+    public static final String REQUIRE_CHANNEL_BINDING = "channel_binding_required";
+    public static final String NOTIFICATION_RINGTONE = "notification_ringtone";
+    public static final String NOTIFICATION_HEADS_UP = "notification_headsup";
+    public static final String NOTIFICATION_VIBRATE = "vibrate_on_notification";
+    public static final String NOTIFICATION_LED = "led";
+    public static final String SHOW_CONNECTION_OPTIONS = "show_connection_options";
+    public static final String USE_TOR = "use_tor";
+    public static final String CHANNEL_DISCOVERY_METHOD = "channel_discovery_method";
+    public static final String SEND_CRASH_REPORTS = "send_crash_reports";
+    public static final String COLORFUL_CHAT_BUBBLES = "use_green_background";
+    public static final String LARGE_FONT = "large_font";
+
+    private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers";
+
+    private final Context context;
+
+    public AppSettings(final Context context) {
+        this.context = context;
+    }
+
+    public Uri getRingtone() {
+        final SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        final String incomingCallRingtone =
+                sharedPreferences.getString(
+                        RINGTONE, context.getString(R.string.incoming_call_ringtone));
+        return Strings.isNullOrEmpty(incomingCallRingtone) ? null : Uri.parse(incomingCallRingtone);
+    }
+
+    public void setRingtone(final Uri uri) {
+        final SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        sharedPreferences.edit().putString(RINGTONE, uri == null ? null : uri.toString()).apply();
+    }
+
+    public Uri getNotificationTone() {
+        final SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        final String incomingCallRingtone =
+                sharedPreferences.getString(
+                        NOTIFICATION_RINGTONE, context.getString(R.string.notification_ringtone));
+        return Strings.isNullOrEmpty(incomingCallRingtone) ? null : Uri.parse(incomingCallRingtone);
+    }
+
+    public void setNotificationTone(final Uri uri) {
+        final SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        sharedPreferences
+                .edit()
+                .putString(NOTIFICATION_RINGTONE, uri == null ? null : uri.toString())
+                .apply();
+    }
+
+    public boolean isBTBVEnabled() {
+        return getBooleanPreference(BTBV, R.bool.btbv);
+    }
+
+    public boolean isTrustSystemCAStore() {
+        return getBooleanPreference(TRUST_SYSTEM_CA_STORE, R.bool.trust_system_ca_store);
+    }
+
+    public boolean isAllowScreenshots() {
+        return getBooleanPreference(ALLOW_SCREENSHOTS, R.bool.allow_screenshots);
+    }
+
+    public boolean isColorfulChatBubbles() {
+        return getBooleanPreference(COLORFUL_CHAT_BUBBLES, R.bool.use_green_background);
+    }
+
+    public boolean isLargeFont() {
+        return getBooleanPreference(LARGE_FONT, R.bool.large_font);
+    }
+
+    public boolean isUseTor() {
+        return getBooleanPreference(USE_TOR, R.bool.use_tor);
+    }
+
+    public boolean isAcceptInvitesFromStrangers() {
+        return getBooleanPreference(
+                ACCEPT_INVITES_FROM_STRANGERS, R.bool.accept_invites_from_strangers);
+    }
+
+    private boolean getBooleanPreference(@NonNull final String name, @BoolRes int res) {
+        final SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        return sharedPreferences.getBoolean(name, context.getResources().getBoolean(res));
+    }
+
+    public String getOmemo() {
+        final SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        return sharedPreferences.getString(
+                OMEMO, context.getString(R.string.omemo_setting_default));
+    }
+
+    public boolean isSendCrashReports() {
+        return getBooleanPreference(SEND_CRASH_REPORTS, R.bool.send_crash_reports);
+    }
+
+    public void setSendCrashReports(boolean value) {
+        final SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        sharedPreferences.edit().putBoolean(SEND_CRASH_REPORTS, value).apply();
+    }
+
+    public boolean isRequireChannelBinding() {
+        return getBooleanPreference(REQUIRE_CHANNEL_BINDING, R.bool.require_channel_binding);
+    }
+}

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

@@ -3,15 +3,15 @@ package eu.siacs.conversations;
 import android.graphics.Bitmap;
 import android.net.Uri;
 
+import eu.siacs.conversations.crypto.XmppDomainVerifier;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 
-import eu.siacs.conversations.crypto.XmppDomainVerifier;
-import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.chatstate.ChatState;
-
 public final class Config {
     private static final int UNENCRYPTED = 1;
     private static final int OPENPGP = 2;
@@ -45,35 +45,34 @@ public final class Config {
 
     public static final Jid BUG_REPORTS = Jid.of("+14169938000@cheogram.com");
     public static final Uri HELP = Uri.parse("https://cheogram.com");
-
-    public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
     public static final String MAGIC_CREATE_DOMAIN = "chatterboxtown.us";
     public static final Jid QUICKSY_DOMAIN = Jid.of("cheogram.com");
     public static final Jid ONBOARDING_DOMAIN = Jid.of("onboarding.cheogram.com");
 
     public static final String CHANNEL_DISCOVERY = "https://search.jabber.network";
 
-    public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
+    public static final boolean DISALLOW_REGISTRATION_IN_UI = false; // hide the register checkbox
 
     public static final boolean USE_RANDOM_RESOURCE_ON_EVERY_BIND = false;
 
-    public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true
+    public static final boolean MESSAGE_DISPLAYED_SYNCHRONIZATION = true;
 
-    public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5;
+    public static final boolean ALLOW_NON_TLS_CONNECTIONS =
+            false; // very dangerous. you should have a good reason to set this to true
 
+    public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5;
 
     public static final boolean QUICKSTART_ENABLED = true;
 
-    //Notification settings
+    // Notification settings
     public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false;
     public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false;
     public static final boolean SUPPRESS_ERROR_NOTIFICATION = false;
 
-
     public static final boolean DISABLE_BAN = false; // disables the ability to ban users from rooms
 
     public static final int PING_MAX_INTERVAL = 300;
-    public static final int IDLE_PING_INTERVAL = 600; //540 is minimum according to docs;
+    public static final int IDLE_PING_INTERVAL = 600; // 540 is minimum according to docs;
     public static final int PING_MIN_INTERVAL = 30;
     public static final int LOW_PING_TIMEOUT = 1; // used after push received
     public static final int PING_TIMEOUT = 15;
@@ -83,10 +82,9 @@ public final class Config {
     public static final int CONNECT_DISCO_TIMEOUT = 20;
     public static final int MINI_GRACE_PERIOD = 750;
 
-    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
+    // 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;
@@ -110,26 +108,37 @@ public final class Config {
 
     public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
 
-    //remove *other* omemo devices from *your* device list announcement after not seeing any activity from them for 42 days. They will automatically add themselves after coming back online.
+    // remove *other* omemo devices from *your* device list announcement after not seeing any
+    // activity from them for 42 days. They will automatically add themselves after coming back
+    // online.
     public static final long OMEMO_AUTO_EXPIRY = 42 * MILLISECONDS_IN_DAY;
 
     public static final boolean REMOVE_BROKEN_DEVICES = false;
     public static final boolean OMEMO_PADDING = false;
     public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
     public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true;
-    public static final boolean DISABLE_PROXY_LOOKUP = false; //disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback)
+    public static final boolean DISABLE_PROXY_LOOKUP =
+            false; // disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback)
     public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
+    public static final boolean USE_JINGLE_MESSAGE_INIT = true;
+
+    public static final boolean JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK = false;
     public static final boolean DISABLE_HTTP_UPLOAD = false;
     public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
-    public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background
-    public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption
+    public static final boolean BACKGROUND_STANZA_LOGGING =
+            false; // log all stanzas that were received while the app is in background
+    public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE =
+            true; // setting to true might increase power consumption
 
     public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
 
-    public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
-    public static final boolean REQUIRE_RTP_VERIFICATION = false; //require a/v calls to be verified with OMEMO
+    public static final boolean X509_VERIFICATION =
+            false; // use x509 certificates to verify OMEMO keys
+    public static final boolean REQUIRE_RTP_VERIFICATION =
+            false; // require a/v calls to be verified with OMEMO
 
-    public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
+    public static final boolean ONLY_INTERNAL_STORAGE =
+            false; // use internal storage instead of sdcard to save attachments
 
     public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
     public static final boolean MUC_LEAVE_BEFORE_JOIN = false;
@@ -145,44 +154,38 @@ public final class Config {
     public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
 
     public static final String[] ENABLED_CIPHERS = {
-            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
-            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",
-            "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256",
-            "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
-            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
-            "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
-
-            "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
-            "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384",
-            "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256",
-            "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
-
-            "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA",
-
-            // Fallback.
-            "TLS_RSA_WITH_AES_128_GCM_SHA256",
-            "TLS_RSA_WITH_AES_128_GCM_SHA384",
-            "TLS_RSA_WITH_AES_256_GCM_SHA256",
-            "TLS_RSA_WITH_AES_256_GCM_SHA384",
-            "TLS_RSA_WITH_AES_128_CBC_SHA256",
-            "TLS_RSA_WITH_AES_128_CBC_SHA384",
-            "TLS_RSA_WITH_AES_256_CBC_SHA256",
-            "TLS_RSA_WITH_AES_256_CBC_SHA384",
-            "TLS_RSA_WITH_AES_128_CBC_SHA",
-            "TLS_RSA_WITH_AES_256_CBC_SHA",
+        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",
+        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256",
+        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+        "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
+        "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
+        "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
+        "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384",
+        "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256",
+        "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
+        "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA",
+
+        // Fallback.
+        "TLS_RSA_WITH_AES_128_GCM_SHA256",
+        "TLS_RSA_WITH_AES_128_GCM_SHA384",
+        "TLS_RSA_WITH_AES_256_GCM_SHA256",
+        "TLS_RSA_WITH_AES_256_GCM_SHA384",
+        "TLS_RSA_WITH_AES_128_CBC_SHA256",
+        "TLS_RSA_WITH_AES_128_CBC_SHA384",
+        "TLS_RSA_WITH_AES_256_CBC_SHA256",
+        "TLS_RSA_WITH_AES_256_CBC_SHA384",
+        "TLS_RSA_WITH_AES_128_CBC_SHA",
+        "TLS_RSA_WITH_AES_256_CBC_SHA",
     };
 
     public static final String[] WEAK_CIPHER_PATTERNS = {
-            "_NULL_",
-            "_EXPORT_",
-            "_anon_",
-            "_RC4_",
-            "_DES_",
-            "_MD5",
+        "_NULL_", "_EXPORT_", "_anon_", "_RC4_", "_DES_", "_MD5",
     };
 
     public static class OMEMO_EXCEPTIONS {
-        //if the own account matches one of the following domains OMEMO won’t be turned on automatically
+        // if the own account matches one of the following domains OMEMO won’t be turned on
+        // automatically
         public static final List<String> ACCOUNT_DOMAINS = Collections.singletonList("s.ms");
 
         //if the contacts domain matches one of the following domains OMEMO won’t be turned on automatically
@@ -202,17 +205,16 @@ public final class Config {
         }
     }
 
-    private Config() {
-    }
+    private Config() {}
 
     public static final class Map {
-        public final static double INITIAL_ZOOM_LEVEL = 4;
-        public final static double FINAL_ZOOM_LEVEL = 15;
-        public final static int MY_LOCATION_INDICATOR_SIZE = 10;
-        public final static int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 3;
-        public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms
-        public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
-        public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
+        public static final double INITIAL_ZOOM_LEVEL = 4;
+        public static final double FINAL_ZOOM_LEVEL = 15;
+        public static final int MY_LOCATION_INDICATOR_SIZE = 10;
+        public static final int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 3;
+        public static final long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms
+        public static final float LOCATION_FIX_SPACE_DELTA = 10; // m
+        public static final int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
     }
 
     // How deep nested quotes should be displayed. '2' means one quote nested in another.

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

@@ -0,0 +1,116 @@
+package eu.siacs.conversations;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import androidx.appcompat.app.AppCompatDelegate;
+
+import com.google.android.material.color.DynamicColors;
+import com.google.android.material.color.DynamicColorsOptions;
+
+import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.utils.ThemeHelper;
+
+public class Conversations extends Application {
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        ExceptionHelper.init(getApplicationContext());
+        applyThemeSettings();
+    }
+
+    public void applyThemeSettings() {
+        final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+        if (sharedPreferences == null) {
+            return;
+        }
+        applyThemeSettings(sharedPreferences);
+    }
+
+    private void applyThemeSettings(final SharedPreferences sharedPreferences) {
+        AppCompatDelegate.setDefaultNightMode(getDesiredNightMode(this, sharedPreferences));
+        var dynamicColorsOptions =
+                new DynamicColorsOptions.Builder()
+                        .setPrecondition((activity, t) -> isDynamicColorsDesired(activity))
+                        .build();
+        DynamicColors.applyToActivitiesIfAvailable(this, dynamicColorsOptions);
+    }
+
+    public static int getDesiredNightMode(final Context context) {
+        final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        if (sharedPreferences == null) {
+            return AppCompatDelegate.getDefaultNightMode();
+        }
+        return getDesiredNightMode(context, sharedPreferences);
+    }
+
+    public static boolean isDynamicColorsDesired(final Context context) {
+        final var preferences = PreferenceManager.getDefaultSharedPreferences(context);
+        return preferences.getBoolean(AppSettings.DYNAMIC_COLORS, false);
+    }
+
+    public static boolean isCustomColorsDesired(final Context context) {
+        final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        final String theme =
+                sharedPreferences.getString(AppSettings.THEME, context.getString(R.string.theme));
+        return "custom".equals(theme);
+    }
+
+    private static int getDesiredNightMode(
+            final Context context, final SharedPreferences sharedPreferences) {
+        var theme =
+                sharedPreferences.getString(AppSettings.THEME, context.getString(R.string.theme));
+
+        // Migrate old themes to equivalent custom
+        if ("oledblack".equals(theme)) {
+            theme = "custom";
+            final var p = PreferenceManager.getDefaultSharedPreferences(context);
+            p
+                .edit()
+                .putString(AppSettings.THEME, "custom")
+                .putBoolean("custom_theme_automatic", false)
+                .putBoolean("custom_theme_dark", true)
+                .putInt("custom_dark_theme_primary", context.getColor(R.color.white))
+                .putInt("custom_dark_theme_primary_dark", context.getColor(android.R.color.black))
+                .putInt("custom_dark_theme_accent", context.getColor(R.color.yeller))
+                .putInt("custom_dark_theme_background_primary", context.getColor(android.R.color.black))
+                .commit();
+        }
+
+        // Migrate old themes to equivalent custom
+        if ("obsidian".equals(theme)) {
+            theme = "custom";
+            final var p = PreferenceManager.getDefaultSharedPreferences(context);
+            p
+                .edit()
+                .putString(AppSettings.THEME, "custom")
+                .putBoolean("custom_theme_automatic", false)
+                .putBoolean("custom_theme_dark", true)
+                .putInt("custom_dark_theme_primary", context.getColor(R.color.black_perpy))
+                .putInt("custom_dark_theme_primary_dark", context.getColor(R.color.black_perpy))
+                .putInt("custom_dark_theme_accent", context.getColor(R.color.yeller))
+                .putInt("custom_dark_theme_background_primary", context.getColor(R.color.blacker_perpy))
+                .commit();
+        }
+
+        if ("custom".equals(theme)) {
+            if (sharedPreferences.getBoolean("custom_theme_automatic", false)) return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
+            return sharedPreferences.getBoolean("custom_theme_dark", false) ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO;
+        }
+
+        return getDesiredNightMode(theme);
+    }
+
+    public static int getDesiredNightMode(final String theme) {
+        if ("automatic".equals(theme)) {
+            return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
+        } else if ("light".equals(theme)) {
+            return AppCompatDelegate.MODE_NIGHT_NO;
+        } else {
+            return AppCompatDelegate.MODE_NIGHT_YES;
+        }
+    }
+}

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

@@ -36,10 +36,10 @@ import android.preference.PreferenceManager;
 
 import com.google.common.base.Strings;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.ui.SettingsActivity;
 
 public class OmemoSetting {
 
@@ -54,13 +54,14 @@ public class OmemoSetting {
 		return encryption;
 	}
 
-	public static void load(final Context context, final SharedPreferences sharedPreferences) {
+	public static void load(final Context context) {
 		if (Config.omemoOnly()) {
 			always = true;
 			encryption = Message.ENCRYPTION_AXOLOTL;
 			return;
 		}
-		final String value = sharedPreferences.getString(SettingsActivity.OMEMO_SETTING, context.getResources().getString(R.string.omemo_setting_default));
+		final var appSettings = new AppSettings(context);
+		final var value = appSettings.getOmemo();
 		switch (Strings.nullToEmpty(value)) {
 			case "always":
 				always = true;
@@ -77,8 +78,4 @@ public class OmemoSetting {
 
 		}
 	}
-
-	public static void load(final Context context) {
-		load(context, PreferenceManager.getDefaultSharedPreferences(context));
-	}
 }

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

@@ -1,17 +1,23 @@
 package eu.siacs.conversations.crypto;
 
+import android.content.Context;
+
 import androidx.annotation.Nullable;
 
 import com.google.common.collect.Iterables;
 
+import java.io.IOException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
 import java.util.Arrays;
 
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509TrustManager;
 
+import eu.siacs.conversations.R;
+
 public final class TrustManagers {
 
     private TrustManagers() {
@@ -34,5 +40,16 @@ public final class TrustManagers {
         return createTrustManager(null);
     }
 
+    public static X509TrustManager defaultWithBundledLetsEncrypt(final Context context)
+            throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
+        final BundledTrustManager bundleTrustManager =
+                BundledTrustManager.builder()
+                        .loadKeyStore(
+                                context.getResources().openRawResource(R.raw.letsencrypt),
+                                "letsencrypt")
+                        .build();
+        return CombiningTrustManager.combineWithDefault(bundleTrustManager);
+    }
+
 
 }

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

@@ -10,15 +10,15 @@ import com.google.common.collect.BiMap;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableBiMap;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.utils.SSLSockets;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
 public enum ChannelBinding {
     NONE,
     TLS_EXPORTER,
@@ -104,22 +104,17 @@ public enum ChannelBinding {
     }
 
     private static String shortName(final ChannelBinding channelBinding) {
-        switch (channelBinding) {
-            case TLS_UNIQUE:
-                return "UNIQ";
-            case TLS_EXPORTER:
-                return "EXPR";
-            case TLS_SERVER_END_POINT:
-                return "ENDP";
-            case NONE:
-                return "NONE";
-            default:
-                throw new AssertionError("Missing short name for " + channelBinding);
-        }
+        return switch (channelBinding) {
+            case TLS_UNIQUE -> "UNIQ";
+            case TLS_EXPORTER -> "EXPR";
+            case TLS_SERVER_END_POINT -> "ENDP";
+            case NONE -> "NONE";
+            default -> throw new AssertionError("Missing short name for " + channelBinding);
+        };
     }
 
     public static int priority(final ChannelBinding channelBinding) {
-        if (Arrays.asList(TLS_EXPORTER,TLS_UNIQUE).contains(channelBinding)) {
+        if (Arrays.asList(TLS_EXPORTER, TLS_UNIQUE).contains(channelBinding)) {
             return 2;
         } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) {
             return 1;

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

@@ -20,12 +20,18 @@ public interface ChannelBindingMechanism {
 
     ChannelBinding getChannelBinding();
 
-    static byte[] getChannelBindingData(final SSLSocket sslSocket, final ChannelBinding channelBinding)
+    static byte[] getChannelBindingData(
+            final SSLSocket sslSocket, final ChannelBinding channelBinding)
             throws SaslMechanism.AuthenticationException {
         if (sslSocket == null) {
-            throw new SaslMechanism.AuthenticationException("Channel binding attempt on non secure socket");
+            throw new SaslMechanism.AuthenticationException(
+                    "Channel binding attempt on non secure socket");
         }
         if (channelBinding == ChannelBinding.TLS_EXPORTER) {
+            if (!Conscrypt.isConscrypt(sslSocket)) {
+                throw new SaslMechanism.AuthenticationException(
+                        "Channel binding attempt on non supporting socket");
+            }
             final byte[] keyingMaterial;
             try {
                 keyingMaterial =
@@ -39,6 +45,10 @@ public interface ChannelBindingMechanism {
             }
             return keyingMaterial;
         } else if (channelBinding == ChannelBinding.TLS_UNIQUE) {
+            if (!Conscrypt.isConscrypt(sslSocket)) {
+                throw new SaslMechanism.AuthenticationException(
+                        "Channel binding attempt on non supporting socket");
+            }
             final byte[] unique = Conscrypt.getTlsUnique(sslSocket);
             if (unique == null) {
                 throw new SaslMechanism.AuthenticationException(
@@ -99,8 +109,7 @@ public interface ChannelBindingMechanism {
     }
 
     static int getPriority(final SaslMechanism mechanism) {
-        if (mechanism instanceof ChannelBindingMechanism) {
-            final ChannelBindingMechanism channelBindingMechanism = (ChannelBindingMechanism) mechanism;
+        if (mechanism instanceof ChannelBindingMechanism channelBindingMechanism) {
             return ChannelBinding.priority(channelBindingMechanism.getChannelBinding());
         } else {
             return 0;

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

@@ -3,6 +3,8 @@ package eu.siacs.conversations.crypto.sasl;
 import android.util.Base64;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMultimap;
@@ -10,8 +12,6 @@ import com.google.common.collect.Multimap;
 import com.google.common.hash.HashFunction;
 import com.google.common.primitives.Bytes;
 
-import org.jetbrains.annotations.NotNull;
-
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collection;
@@ -168,7 +168,7 @@ public abstract class HashedToken extends SaslMechanism implements ChannelBindin
             return null;
         }
 
-        @NotNull
+        @NonNull
         @Override
         public String toString() {
             return MoreObjects.toStringHelper(this)

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

@@ -6,17 +6,17 @@ import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
 
-import java.util.Collection;
-import java.util.Collections;
-
-import javax.net.ssl.SSLSocket;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.utils.SSLSockets;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.net.ssl.SSLSocket;
+
 public abstract class SaslMechanism {
 
     protected final Account account;
@@ -170,7 +170,9 @@ public abstract class SaslMechanism {
     }
 
     public static SaslMechanism ensureAvailable(
-            final SaslMechanism mechanism, final SSLSockets.Version sslVersion) {
+            final SaslMechanism mechanism,
+            final SSLSockets.Version sslVersion,
+            final boolean requireChannelBinding) {
         if (mechanism instanceof ChannelBindingMechanism) {
             final ChannelBinding cb = ((ChannelBindingMechanism) mechanism).getChannelBinding();
             if (ChannelBinding.isAvailable(cb, sslVersion)) {
@@ -181,6 +183,9 @@ public abstract class SaslMechanism {
                         "pinned channel binding method " + cb + " no longer available");
                 return null;
             }
+        } else if (requireChannelBinding) {
+            Log.d(Config.LOGTAG, "pinned mechanism did not provide channel binding");
+            return null;
         } else {
             return mechanism;
         }

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

@@ -5,6 +5,9 @@ import android.content.Context;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -213,7 +216,7 @@ public class Bookmark extends Element implements ListItem {
 		for (Element element : getChildren()) {
 			if (element.getName().equals("group") && element.getContent() != null) {
 				String group = element.getContent();
-				tags.add(new Tag(group, UIHelper.getColorForName(group, true)));
+				tags.add(new Tag(group));
 			}
 		}
 
@@ -223,7 +226,7 @@ public class Bookmark extends Element implements ListItem {
 	@Override
 	public List<Tag> getTags(Context context) {
 		ArrayList<Tag> tags = new ArrayList<>();
-		tags.add(new Tag("Channel", UIHelper.getColorForName("Channel",true)));
+		tags.add(new Tag("Channel"));
 		tags.addAll(getGroupTags());
 		return tags;
 	}

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

@@ -206,7 +206,7 @@ public class Contact implements ListItem, Blockable {
     public List<Tag> getGroupTags() {
         final ArrayList<Tag> tags = new ArrayList<>();
         for (final String group : getGroups(true)) {
-            tags.add(new Tag(group, UIHelper.getColorForName(group)));
+            tags.add(new Tag(group));
         }
         return tags;
     }
@@ -216,17 +216,11 @@ public class Contact implements ListItem, Blockable {
         final HashSet<Tag> tags = new HashSet<>();
         tags.addAll(getGroupTags());
         for (final String tag : getSystemTags(true)) {
-            tags.add(new Tag(tag, UIHelper.getColorForName(tag)));
+            tags.add(new Tag(tag));
         }
         Presence.Status status = getShownStatus();
-        if (status != Presence.Status.OFFLINE) {
-            tags.add(UIHelper.getTagForStatus(context, status));
-        }
-        if (isBlocked()) {
-            tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
-        }
         if (!showInRoster() && getSystemAccount() != null) {
-            tags.add(new Tag("Android", UIHelper.getColorForName("Android")));
+            tags.add(new Tag("Android"));
         }
         return new ArrayList<>(tags);
     }

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

@@ -68,10 +68,12 @@ import com.cheogram.android.ConversationPage;
 import com.cheogram.android.Util;
 import com.cheogram.android.WebxdcPage;
 
+import com.google.android.material.color.MaterialColors;
 import com.google.android.material.tabs.TabLayout;
 import com.google.android.material.textfield.TextInputLayout;
 import com.google.common.base.Optional;
 import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
 import io.ipfs.cid.Cid;
@@ -204,6 +206,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     protected boolean userSelectedThread = false;
     protected Message replyTo = null;
     protected HashMap<String, Thread> threads = new HashMap<>();
+    private String displayState = null;
 
     public Conversation(final String name, final Account account, final Jid contactJid,
                         final int mode) {
@@ -555,6 +558,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         return null;
     }
 
+    public Message findReceivedWithRemoteId(final String id) {
+        synchronized (this.messages) {
+            for (final Message message : this.messages) {
+                if (message.getStatus() == Message.STATUS_RECEIVED && id.equals(message.getRemoteMsgId())) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
     public Message findMessageWithServerMsgId(String id) {
         synchronized (this.messages) {
             for (Message message : this.messages) {
@@ -836,20 +850,20 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         }
     }
 
-    public List<Message> markRead(String upToUuid) {
-        final List<Message> unread = new ArrayList<>();
+    public List<Message> markRead(final String upToUuid) {
+        final ImmutableList.Builder<Message> unread = new ImmutableList.Builder<>();
         synchronized (this.messages) {
-            for (Message message : this.messages) {
+            for (final Message message : this.messages) {
                 if (!message.isRead()) {
                     message.markRead();
                     unread.add(message);
                 }
                 if (message.getUuid().equals(upToUuid)) {
-                    return unread;
+                    return unread.build();
                 }
             }
         }
-        return unread;
+        return unread.build();
     }
 
     public Message getLatestMessage() {
@@ -1426,6 +1440,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         pagerAdapter.hide();
     }
 
+    public void setDisplayState(final String stanzaId) {
+        this.displayState = stanzaId;
+    }
+
+    public String getDisplayState() {
+        return this.displayState;
+    }
+
     public interface OnMessageFound {
         void onMessageFound(final Message message);
     }
@@ -1938,7 +1960,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     Cell cell = (Cell) item;
 
                     if (cell.el == null) {
-                        binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Subhead);
+                        binding.text.setTextAppearance(binding.getRoot().getContext(), com.google.android.material.R.style.TextAppearance_Material3_TitleMedium);
                         setTextOrHide(binding.text, cell.reported.getLabel());
                     } else {
                         Element validate = cell.reported.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
@@ -1955,7 +1977,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                             } catch (final IllegalArgumentException | NumberParseException | NullPointerException e) { }
                         }
 
-                        binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Body1);
+                        binding.text.setTextAppearance(binding.getRoot().getContext(), com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
                         binding.text.setText(text);
 
                         BetterLinkMovementMethod method = BetterLinkMovementMethod.newInstance();
@@ -2041,6 +2063,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 protected ArrayAdapter<Option> adapter;
                 protected boolean open;
                 protected boolean multi;
+                protected int textColor = -1;
 
                 @Override
                 public void bind(Item item) {
@@ -2057,12 +2080,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     setTextOrHide(binding.label, field.getLabel());
                     setTextOrHide(binding.desc, field.getDesc());
 
+                    if (textColor == -1) textColor = binding.desc.getCurrentTextColor();
                     if (field.error != null) {
                         binding.desc.setVisibility(View.VISIBLE);
                         binding.desc.setText(field.error);
-                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
+                        binding.desc.setTextColor(com.google.android.material.R.attr.colorError);
                     } else {
-                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
+                        binding.desc.setTextColor(textColor);
                     }
 
                     Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
@@ -2147,6 +2171,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 }
                 protected Element mValue = null;
                 protected ArrayAdapter<Option> options;
+                protected int textColor = -1;
 
                 @Override
                 public void bind(Item item) {
@@ -2154,12 +2179,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     setTextOrHide(binding.label, field.getLabel());
                     setTextOrHide(binding.desc, field.getDesc());
 
+                    if (textColor == -1) textColor = binding.desc.getCurrentTextColor();
                     if (field.error != null) {
                         binding.desc.setVisibility(View.VISIBLE);
                         binding.desc.setText(field.error);
-                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
+                        binding.desc.setTextColor(com.google.android.material.R.attr.colorError);
                     } else {
-                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
+                        binding.desc.setTextColor(textColor);
                     }
 
                     mValue = field.getValue();
@@ -2295,6 +2321,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 protected ArrayAdapter<Option> options;
                 protected Option defaultOption = null;
                 protected boolean mediaSelector = false;
+                protected int textColor = -1;
 
                 @Override
                 public void bind(Item item) {
@@ -2302,12 +2329,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     setTextOrHide(binding.label, field.getLabel());
                     setTextOrHide(binding.desc, field.getDesc());
 
+                    if (textColor == -1) textColor = binding.desc.getCurrentTextColor();
                     if (field.error != null) {
                         binding.desc.setVisibility(View.VISIBLE);
                         binding.desc.setText(field.error);
-                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
+                        binding.desc.setTextColor(com.google.android.material.R.attr.colorError);
                     } else {
-                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
+                        binding.desc.setTextColor(textColor);
                     }
 
                     mValue = field.getValue();
@@ -2748,7 +2776,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     int resId = ctx.getResources().getIdentifier("action_" + getItem(position).first, "string" , ctx.getPackageName());
                     if (resId != 0 && getItem(position).second.equals(getItem(position).first)) tv.setText(ctx.getResources().getString(resId));
                     tv.setTextColor(ContextCompat.getColor(ctx, R.color.white));
-                    tv.setBackgroundColor(UIHelper.getColorForName(getItem(position).first));
+                    tv.setBackgroundColor(MaterialColors.harmonizeWithPrimary(ctx,UIHelper.getColorForName(getItem(position).first)));
                     return v;
                 }
 
@@ -2829,18 +2857,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 mNode = node;
                 this.xmppConnectionService = xmppConnectionService;
                 if (mPager != null) setupLayoutManager();
-                actionsAdapter = new ActionsAdapter(xmppConnectionService);
-                actionsAdapter.registerDataSetObserver(new DataSetObserver() {
-                    @Override
-                    public void onChanged() {
-                        if (mBinding == null) return;
-
-                        mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
-                    }
-
-                    @Override
-                    public void onInvalidated() {}
-                });
             }
 
             public String getTitle() {
@@ -3407,6 +3423,19 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 });
                 mBinding.form.setLayoutManager(setupLayoutManager());
                 mBinding.form.setAdapter(this);
+
+                actionsAdapter = new ActionsAdapter(mBinding.getRoot().getContext());
+                actionsAdapter.registerDataSetObserver(new DataSetObserver() {
+                    @Override
+                    public void onChanged() {
+                        if (mBinding == null) return;
+
+                        mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
+                    }
+
+                    @Override
+                    public void onInvalidated() {}
+                });
                 mBinding.actions.setAdapter(actionsAdapter);
                 mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
                     if (execute(pos)) {

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

@@ -22,15 +22,9 @@ public interface ListItem extends Comparable<ListItem>, AvatarService.Avatarable
 
 	final class Tag implements Serializable {
 		private final String name;
-		private final int color;
 
-		public Tag(final String name, final int color) {
+		public Tag(final String name) {
 			this.name = name;
-			this.color = color;
-		}
-
-		public int getColor() {
-			return this.color;
 		}
 
 		public String getName() {
@@ -44,7 +38,7 @@ public interface ListItem extends Comparable<ListItem>, AvatarService.Avatarable
 		public boolean equals(Object o) {
 			if (!(o instanceof Tag)) return false;
 			Tag ot = (Tag) o;
-			return name.toLowerCase(Locale.US).equals(ot.getName().toLowerCase(Locale.US)) && color == ot.getColor();
+			return name.toLowerCase(Locale.US).equals(ot.getName().toLowerCase(Locale.US));
 		}
 
 		public int hashCode() {

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

@@ -83,6 +83,12 @@ public class Presences {
         }
     }
 
+    public boolean isEmpty() {
+        synchronized (this.presences) {
+            return this.presences.isEmpty();
+        }
+    }
+
     public String[] toResourceArray() {
         synchronized (this.presences) {
             final String[] presencesArray = new String[presences.size()];

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

@@ -41,18 +41,18 @@ public class RtpSessionStatus {
         return new RtpSessionStatus(made, duration);
     }
 
-    public static @DrawableRes int getDrawable(final boolean received, final boolean successful, final boolean darkTheme) {
+    public static @DrawableRes int getDrawable(final boolean received, final boolean successful) {
         if (received) {
             if (successful) {
-                return darkTheme ? R.drawable.ic_call_received_white_18dp : R.drawable.ic_call_received_black_18dp;
+                return R.drawable.ic_call_received_24dp;
             } else {
-                return darkTheme ? R.drawable.ic_call_missed_white_18dp : R.drawable.ic_call_missed_black_18dp;
+                return R.drawable.ic_call_missed_24db;
             }
         } else {
             if (successful) {
-                return darkTheme ? R.drawable.ic_call_made_white_18dp : R.drawable.ic_call_made_black_18dp;
+                return R.drawable.ic_call_made_24dp;
             } else {
-                return darkTheme ? R.drawable.ic_call_missed_outgoing_white_18dp : R.drawable.ic_call_missed_outgoing_black_18dp;
+                return R.drawable.ic_call_missed_outgoing_24dp;
             }
         }
     }

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

@@ -2,6 +2,15 @@ package eu.siacs.conversations.generator;
 
 import android.util.Base64;
 
+import eu.siacs.conversations.BuildConfig;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.text.SimpleDateFormat;
@@ -12,54 +21,41 @@ import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
 
-import eu.siacs.conversations.BuildConfig;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.PhoneHelper;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.XmppConnection;
-import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
-
 public abstract class AbstractGenerator {
-    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
-    private final String[] FEATURES = {
-            Namespace.JINGLE,
-            Namespace.JINGLE_APPS_FILE_TRANSFER,
-            Namespace.JINGLE_TRANSPORTS_S5B,
-            Namespace.JINGLE_TRANSPORTS_IBB,
-            Namespace.JINGLE_ENCRYPTED_TRANSPORT,
-            Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
-            "http://jabber.org/protocol/muc",
-            "jabber:x:conference",
-            Namespace.OOB,
-            "http://jabber.org/protocol/caps",
-            "http://jabber.org/protocol/disco#info",
-            "urn:xmpp:avatar:metadata+notify",
-            Namespace.NICK + "+notify",
-            "urn:xmpp:ping",
-            "jabber:iq:version",
-            "http://jabber.org/protocol/chatstates"
+    private static final SimpleDateFormat DATE_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+    private final String[] STATIC_FEATURES = {
+        Namespace.JINGLE,
+        Namespace.JINGLE_APPS_FILE_TRANSFER,
+        Namespace.JINGLE_TRANSPORTS_S5B,
+        Namespace.JINGLE_TRANSPORTS_IBB,
+        Namespace.JINGLE_ENCRYPTED_TRANSPORT,
+        Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
+        "http://jabber.org/protocol/muc",
+        "jabber:x:conference",
+        Namespace.OOB,
+        "http://jabber.org/protocol/caps",
+        "http://jabber.org/protocol/disco#info",
+        "urn:xmpp:avatar:metadata+notify",
+        Namespace.NICK + "+notify",
+        "urn:xmpp:ping",
+        "jabber:iq:version",
+        "http://jabber.org/protocol/chatstates"
     };
     private final String[] MESSAGE_CONFIRMATION_FEATURES = {
-            "urn:xmpp:chat-markers:0",
-            "urn:xmpp:receipts"
-    };
-    private final String[] MESSAGE_CORRECTION_FEATURES = {
-            "urn:xmpp:message-correct:0"
+        "urn:xmpp:chat-markers:0", "urn:xmpp:receipts"
     };
+    private final String[] MESSAGE_CORRECTION_FEATURES = {"urn:xmpp:message-correct:0"};
     private final String[] PRIVACY_SENSITIVE = {
-            "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
+        "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone
     };
     private final String[] VOIP_NAMESPACES = {
-            Namespace.JINGLE_TRANSPORT_ICE_UDP,
-            Namespace.JINGLE_FEATURE_AUDIO,
-            Namespace.JINGLE_FEATURE_VIDEO,
-            Namespace.JINGLE_APPS_RTP,
-            Namespace.JINGLE_APPS_DTLS,
-            Namespace.JINGLE_MESSAGE
+        Namespace.JINGLE_TRANSPORT_ICE_UDP,
+        Namespace.JINGLE_FEATURE_AUDIO,
+        Namespace.JINGLE_FEATURE_VIDEO,
+        Namespace.JINGLE_APPS_RTP,
+        Namespace.JINGLE_APPS_DTLS,
+        Namespace.JINGLE_MESSAGE
     };
     protected XmppConnectionService mXmppConnectionService;
 
@@ -90,7 +86,11 @@ public abstract class AbstractGenerator {
 
     String getCapHash(final Account account) {
         StringBuilder s = new StringBuilder();
-        s.append("client/").append(getIdentityType()).append("//").append(getIdentityName()).append('<');
+        s.append("client/")
+                .append(getIdentityType())
+                .append("//")
+                .append(getIdentityName())
+                .append('<');
         MessageDigest md;
         try {
             md = MessageDigest.getInstance("SHA-1");
@@ -107,9 +107,12 @@ public abstract class AbstractGenerator {
 
     public List<String> getFeatures(Account account) {
         final XmppConnection connection = account.getXmppConnection();
-        final ArrayList<String> features = new ArrayList<>(Arrays.asList(FEATURES));
+        final ArrayList<String> features = new ArrayList<>(Arrays.asList(STATIC_FEATURES));
         features.add("http://jabber.org/protocol/xhtml-im");
         features.add("urn:xmpp:bob");
+        if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {
+            features.add(Namespace.MDS_DISPLAYED + "+notify");
+        }
         if (mXmppConnectionService.confirmMessages()) {
             features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
         }

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

@@ -157,6 +157,10 @@ public class IqGenerator extends AbstractGenerator {
         return retrieve(Namespace.BOOKMARKS2, null);
     }
 
+    public IqPacket retrieveMds() {
+        return retrieve(Namespace.MDS_DISPLAYED, null);
+    }
+
     public IqPacket publishNick(String nick) {
         final Element item = new Element("item");
         item.setAttribute("id", "current");
@@ -292,6 +296,24 @@ public class IqGenerator extends AbstractGenerator {
         return conference;
     }
 
+    public Element mdsDisplayed(final String stanzaId, final Conversation conversation) {
+        final Jid by;
+        if (conversation.getMode() == Conversation.MODE_MULTI) {
+            by = conversation.getJid().asBareJid();
+        } else {
+            by = conversation.getAccount().getJid().asBareJid();
+        }
+        return mdsDisplayed(stanzaId, by);
+    }
+
+    private Element mdsDisplayed(final String stanzaId, final Jid by) {
+        final Element displayed = new Element("displayed", Namespace.MDS_DISPLAYED);
+        final Element stanzaIdElement = displayed.addChild("stanza-id", Namespace.STANZA_IDS);
+        stanzaIdElement.setAttribute("id", stanzaId);
+        stanzaIdElement.setAttribute("by", by);
+        return displayed;
+    }
+
     public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
                                    final Set<PreKeyRecord> preKeyRecords, final int deviceId, Bundle publishOptions) {
         final Element item = new Element("item");

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

@@ -22,6 +22,7 @@ import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
 public class MessageGenerator extends AbstractGenerator {
@@ -266,6 +267,19 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
+    public MessagePacket sessionFinish(
+            final Jid with, final String sessionId, final Reason reason) {
+        final MessagePacket packet = new MessagePacket();
+        packet.setType(MessagePacket.TYPE_CHAT);
+        packet.setTo(with);
+        final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
+        finish.setAttribute("id", sessionId);
+        final Element reasonElement = finish.addChild("reason", Namespace.JINGLE);
+        reasonElement.addChild(reason.toString());
+        packet.addChild("store", "urn:xmpp:hints");
+        return packet;
+    }
+
     public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
         final MessagePacket packet = new MessagePacket();
         packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
@@ -276,7 +290,6 @@ public class MessageGenerator extends AbstractGenerator {
         for (final Media media : proposal.media) {
             propose.addChild("description", Namespace.JINGLE_APPS_RTP).setAttribute("media", media.toString());
         }
-
         packet.addChild("request", "urn:xmpp:receipts");
         packet.addChild("store", "urn:xmpp:hints");
         return packet;

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java 🔗

@@ -93,8 +93,7 @@ public class HttpDownloadConnection implements Transferable {
             final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
             if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
                 this.message.setEncryption(Message.ENCRYPTION_PGP);
-            } else if (message.getEncryption() != Message.ENCRYPTION_OTR
-                    && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
+            } else if (message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
                 this.message.setEncryption(Message.ENCRYPTION_NONE);
             }
             String ext = extension.getExtension();

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java 🔗

@@ -123,8 +123,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
         final long originalFileSize = file.getSize();
         this.delayed = delay;
         if (Config.ENCRYPT_ON_HTTP_UPLOADED
-                || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
-                || message.getEncryption() == Message.ENCRYPTION_OTR) {
+                || message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
             this.key = new byte[44];
             SECURE_RANDOM.nextBytes(this.key);
             this.file.setKeyAndIv(this.key);

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

@@ -23,6 +23,7 @@ import java.util.UUID;
 
 import io.ipfs.cid.Cid;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -64,7 +65,7 @@ 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", "ringing");
+            Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
 
     public MessageParser(XmppConnectionService service) {
         super(service);
@@ -282,8 +283,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                     mXmppConnectionService.updateConversationUi();
                 }
             }
+        } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
+                && Namespace.MDS_DISPLAYED.equals(node)
+                && account.getJid().asBareJid().equals(from)) {
+            final Element item = items.findChild("item");
+            mXmppConnectionService.processMdsItem(account, item);
         } else {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " received pubsub notification for node=" + node);
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + " received pubsub notification for node="
+                            + node);
         }
     }
 
@@ -389,6 +399,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         final Element result = MessageArchiveService.Version.findResult(original);
         final String queryId = result == null ? null : result.getAttribute("queryid");
         final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
+        final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved();
         if (query != null && query.validFrom(original.getFrom())) {
             final Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", query.version.namespace);
             if (f == null) {
@@ -490,7 +501,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             if (selfAddressed) {
                 counterpart = from;
             } else {
-                counterpart = to != null ? to : account.getJid();
+                counterpart = to;
             }
         } else {
             status = Message.STATUS_RECEIVED;
@@ -1026,7 +1037,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                         if (sessionId == null) {
                             break;
                         }
-                        if (query == null) {
+                        if (query == null && offlineMessagesRetrieved) {
                             if (serverMsgId == null) {
                                 serverMsgId = extractStanzaId(account, packet);
                             }
@@ -1047,24 +1058,25 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                                     && contact.showInContactList()) {
                                 processMessageReceipts(account, packet, remoteMsgId, null);
                             }
-                        } else if (query.isCatchup()) {
+                        } else if ((query != null && query.isCatchup()) || !offlineMessagesRetrieved) {
                             if ("propose".equals(action)) {
                                 final Element description = child.findChild("description");
-                                final String namespace = description == null ? null : description.getNamespace();
+                                final String namespace =
+                                        description == null ? null : description.getNamespace();
                                 if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
-                                    final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false);
-                                    final Message preExistingMessage = c.findRtpSession(sessionId, status);
+                                    final Conversation c =
+                                            mXmppConnectionService.findOrCreateConversation(
+                                                    account, counterpart.asBareJid(), false, false);
+                                    final Message preExistingMessage =
+                                            c.findRtpSession(sessionId, status);
                                     if (preExistingMessage != null) {
                                         preExistingMessage.setServerMsgId(serverMsgId);
                                         mXmppConnectionService.updateMessage(preExistingMessage);
                                         break;
                                     }
-                                    final Message message = new Message(
-                                            c,
-                                            status,
-                                            Message.TYPE_RTP_SESSION,
-                                            sessionId
-                                    );
+                                    final Message message =
+                                            new Message(
+                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
                                     message.setServerMsgId(serverMsgId);
                                     message.setTime(timestamp);
                                     message.setBody(new RtpSessionStatus(false, 0).toString());
@@ -1072,9 +1084,14 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                                     mXmppConnectionService.databaseBackend.createMessage(message);
                                 }
                             } else if ("proceed".equals(action)) {
-                                //status needs to be flipped to find the original propose
-                                final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false);
-                                final int s = packet.fromAccount(account) ? Message.STATUS_RECEIVED : Message.STATUS_SEND;
+                                // status needs to be flipped to find the original propose
+                                final Conversation c =
+                                        mXmppConnectionService.findOrCreateConversation(
+                                                account, counterpart.asBareJid(), false, false);
+                                final int s =
+                                        packet.fromAccount(account)
+                                                ? Message.STATUS_RECEIVED
+                                                : Message.STATUS_SEND;
                                 final Message message = c.findRtpSession(sessionId, s);
                                 if (message != null) {
                                     message.setBody(new RtpSessionStatus(true, 0).toString());
@@ -1084,9 +1101,15 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                                     message.setTime(timestamp);
                                     mXmppConnectionService.updateMessage(message, true);
                                 } else {
-                                    Log.d(Config.LOGTAG, "unable to find original rtp session message for received propose");
+                                    Log.d(
+                                            Config.LOGTAG,
+                                            "unable to find original rtp session message for received propose");
                                 }
 
+                            } else if ("finish".equals(action)) {
+                                Log.d(
+                                        Config.LOGTAG,
+                                        "received JMI 'finish' during MAM catch-up. Can be used to update success/failure and duration");
                             }
                         } else {
                             //MAM reloads (non catchups
@@ -1146,12 +1169,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                 }
             }
         }
-        Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
+        final Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
         if (displayed != null) {
             final String id = displayed.getAttribute("id");
             final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
             if (packet.fromAccount(account) && !selfAddressed) {
-                dismissNotification(account, counterpart, query, id);
+                final Conversation c =
+                        mXmppConnectionService.find(account, counterpart.asBareJid());
+                final Message message =
+                        (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
+                if (message != null && (query == null || query.isCatchup())) {
+                    mXmppConnectionService.markReadUpTo(c, message);
+                }
                 if (query == null) {
                     activateGracePeriod(account);
                 }
@@ -1173,7 +1202,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                     final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
                     if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
                         if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
-                            mXmppConnectionService.markRead(conversation);
+                            mXmppConnectionService.markReadUpTo(conversation, message);
                         }
                     } else if (!counterpart.isBareJid() && trueJid != null) {
                         final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
@@ -1282,22 +1311,48 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             if (this.jid == null) {
                 return false;
             }
-            final Contact contact = this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
+            final Contact contact =
+                    this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
             if (contact != null && contact.isBlocked()) {
-                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite from "+contact.getJid()+" because contact is blocked");
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + ": ignore invite from "
+                                + contact.getJid()
+                                + " because contact is blocked");
                 return false;
             }
-            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
-            if (conversation.getMucOptions().online()) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online");
-                mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+            final AppSettings appSettings = new AppSettings(mXmppConnectionService);
+            if ((contact != null && contact.showInContactList())
+                    || appSettings.isAcceptInvitesFromStrangers()) {
+                final Conversation conversation =
+                        mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
+                if (conversation.getMucOptions().online()) {
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid()
+                                    + ": received invite to "
+                                    + jid
+                                    + " but muc is considered to be online");
+                    mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+                } else {
+                    conversation.getMucOptions().setPassword(password);
+                    mXmppConnectionService.databaseBackend.updateConversation(conversation);
+                    mXmppConnectionService.joinMuc(
+                            conversation, contact != null && contact.showInContactList());
+                    mXmppConnectionService.updateConversationUi();
+                }
+                return true;
             } else {
-                conversation.getMucOptions().setPassword(password);
-                mXmppConnectionService.databaseBackend.updateConversation(conversation);
-                mXmppConnectionService.joinMuc(conversation, contact != null && contact.showInContactList());
-                mXmppConnectionService.updateConversationUi();
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + ": ignoring invite from "
+                                + this.inviter
+                                + " because we are not accepting invites from strangers. direct="
+                                + direct);
+                return false;
             }
-            return true;
         }
     }
 }

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

@@ -400,29 +400,35 @@ public class FileBackend {
         }
     }
 
-    public static boolean weOwnFile(final Uri uri) {
-        if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
-            return false;
-        } else {
-            return weOwnFileLollipop(uri);
+    public static boolean dangerousFile(final Uri uri) {
+        if (uri == null || Strings.isNullOrEmpty(uri.getScheme())) {
+            return true;
+        }
+        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                // On Android 7 (and apps that target 7) it is now longer possible to share files
+                // with a file scheme. By now you should probably not be running apps that target
+                // anything less than 7 any more
+                return true;
+            } else {
+                return isFileOwnedByProcess(uri);
+            }
         }
+        return false;
     }
 
-    private static boolean weOwnFileLollipop(final Uri uri) {
+    private static boolean isFileOwnedByProcess(final Uri uri) {
         final String path = uri.getPath();
         if (path == null) {
-            return false;
+            return true;
         }
-        try {
-            File file = new File(path);
-            FileDescriptor fd =
-                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
-                            .getFileDescriptor();
-            StructStat st = Os.fstat(fd);
+        try (final var pfd =
+                ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY)) {
+            final FileDescriptor fd = pfd.getFileDescriptor();
+            final StructStat st = Os.fstat(fd);
             return st.st_uid == android.os.Process.myUid();
-        } catch (FileNotFoundException e) {
-            return false;
-        } catch (Exception e) {
+        } catch (final Exception e) {
+            // when in doubt. better safe than sorry
             return true;
         }
     }

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

@@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.google.common.base.MoreObjects;
@@ -14,9 +15,6 @@ import com.google.common.base.Objects;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
 import java.util.List;
 
 import eu.siacs.conversations.Config;
@@ -254,7 +252,7 @@ public class UnifiedPushDatabase extends SQLiteOpenHelper {
             this.instance = instance;
         }
 
-        @NotNull
+        @NonNull
         @Override
         public String toString() {
             return MoreObjects.toStringHelper(this)

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

@@ -15,237 +15,136 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.media.AudioDeviceInfo;
-import android.media.AudioFormat;
 import android.media.AudioManager;
-import android.media.AudioRecord;
-import android.media.MediaRecorder;
-import android.os.Build;
+import android.media.ToneGenerator;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.google.common.collect.ImmutableSet;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.AppRTCUtils;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 
 import org.webrtc.ThreadUtils;
 
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.utils.AppRTCUtils;
-import eu.siacs.conversations.xmpp.jingle.Media;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 
-/**
- * AppRTCAudioManager manages all audio related parts of the AppRTC demo.
- */
+/** AppRTCAudioManager manages all audio related parts of the AppRTC demo. */
 public class AppRTCAudioManager {
 
-    private static CountDownLatch microphoneLatch;
-
     private final Context apprtcContext;
     // Contains speakerphone setting: auto, true or false
-    @Nullable
-    private SpeakerPhonePreference speakerPhonePreference;
     // Handles all tasks related to Bluetooth headset devices.
     private final AppRTCBluetoothManager bluetoothManager;
-    @Nullable
-    private final AudioManager audioManager;
-    @Nullable
-    private AudioManagerEvents audioManagerEvents;
+    @Nullable private final AudioManager audioManager;
+    @Nullable private AudioManagerEvents audioManagerEvents;
     private AudioManagerState amState;
     private boolean savedIsSpeakerPhoneOn;
     private boolean savedIsMicrophoneMute;
     private boolean hasWiredHeadset;
     // Default audio device; speaker phone for video calls or earpiece for audio
     // only calls.
-    private AudioDevice defaultAudioDevice;
+    private CallIntegration.AudioDevice defaultAudioDevice;
     // Contains the currently selected audio device.
     // This device is changed automatically using a certain scheme where e.g.
     // a wired headset "wins" over speaker phone. It is also possible for a
     // user to explicitly select a device (and overrid any predefined scheme).
     // See |userSelectedAudioDevice| for details.
-    private AudioDevice selectedAudioDevice;
+    private CallIntegration.AudioDevice selectedAudioDevice;
     // Contains the user-selected audio device which overrides the predefined
     // selection scheme.
     // TODO(henrika): always set to AudioDevice.NONE today. Add support for
     // explicit selection based on choice by userSelectedAudioDevice.
-    private AudioDevice userSelectedAudioDevice;
-    // Proximity sensor object. It measures the proximity of an object in cm
-    // relative to the view screen of a device and can therefore be used to
-    // assist device switching (close to ear <=> use headset earpiece if
-    // available, far from ear <=> use speaker phone).
-    @Nullable
-    private AppRTCProximitySensor proximitySensor;
+    private CallIntegration.AudioDevice userSelectedAudioDevice;
+
     // Contains a list of available audio devices. A Set collection is used to
     // avoid duplicate elements.
-    private Set<AudioDevice> audioDevices = new HashSet<>();
+    private Set<CallIntegration.AudioDevice> audioDevices = new HashSet<>();
     // Broadcast receiver for wired headset intent broadcasts.
     private final BroadcastReceiver wiredHeadsetReceiver;
     // Callback method for changes in audio focus.
-    @Nullable
-    private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
+    @Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
+    private ScheduledFuture<?> ringBackFuture;
 
-    private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) {
-        Log.d(Config.LOGTAG, "ctor");
-        ThreadUtils.checkIsOnMainThread();
+    public AppRTCAudioManager(final Context context) {
         apprtcContext = context;
         audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
         bluetoothManager = AppRTCBluetoothManager.create(context, this);
         wiredHeadsetReceiver = new WiredHeadsetReceiver();
         amState = AudioManagerState.UNINITIALIZED;
-        this.speakerPhonePreference = speakerPhonePreference;
-        if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
-            defaultAudioDevice = AudioDevice.EARPIECE;
+        // CallIntegration / Connection uses Earpiece as default too
+        if (hasEarpiece()) {
+            defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
         } else {
-            defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+            defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
         }
-        // Create and initialize the proximity sensor.
-        // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
-        // Note that, the sensor will not be active until start() has been called.
-        proximitySensor = AppRTCProximitySensor.create(context,
-                // This method will be called each time a state change is detected.
-                // Example: user holds his hand over the device (closer than ~5 cm),
-                // or removes his hand from the device.
-                this::onProximitySensorChangedState);
         Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
         AppRTCUtils.logDeviceInfo(Config.LOGTAG);
     }
 
-    public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
-        this.speakerPhonePreference = speakerPhonePreference;
-        if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
-            defaultAudioDevice = AudioDevice.EARPIECE;
-        } else {
-            defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
-        }
-        updateAudioDeviceState();
-    }
-
-    /**
-     * Construction.
-     */
-    public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
-        return new AppRTCAudioManager(context, speakerPhonePreference);
-    }
-
-    public static boolean isMicrophoneAvailable() {
-        microphoneLatch = new CountDownLatch(1);
-        AudioRecord audioRecord = null;
-        boolean available = true;
-        try {
-            final int sampleRate = 44100;
-            final int channel = AudioFormat.CHANNEL_IN_MONO;
-            final int format = AudioFormat.ENCODING_PCM_16BIT;
-            final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
-            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize);
-            audioRecord.startRecording();
-            final short[] buffer = new short[bufferSize];
-            final int audioStatus = audioRecord.read(buffer, 0, bufferSize);
-            if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED)
-                available = false;
-        } catch (Exception e) {
-            available = false;
-        } finally {
-            release(audioRecord);
-
-        }
-        microphoneLatch.countDown();
-        return available;
-    }
-
-    private static void release(final AudioRecord audioRecord) {
-        if (audioRecord == null) {
-            return;
-        }
-        try {
-            audioRecord.release();
-        } catch (Exception e) {
-            //ignore
-        }
-    }
-
-    /**
-     * This method is called when the proximity sensor reports a state change,
-     * e.g. from "NEAR to FAR" or from "FAR to NEAR".
-     */
-    private void onProximitySensorChangedState() {
-        if (speakerPhonePreference != SpeakerPhonePreference.AUTO) {
-            return;
-        }
-        // The proximity sensor should only be activated when there are exactly two
-        // available audio devices.
-        if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
-                && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
-            if (proximitySensor.sensorReportsNearState()) {
-                // Sensor reports that a "handset is being held up to a person's ear",
-                // or "something is covering the light sensor".
-                setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE);
-            } else {
-                // Sensor reports that a "handset is removed from a person's ear", or
-                // "the light sensor is no longer covered".
-                setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
-            }
-        }
+    public void setAudioManagerEvents(final AudioManagerEvents audioManagerEvents) {
+        this.audioManagerEvents = audioManagerEvents;
     }
 
     @SuppressWarnings("deprecation")
-    public void start(AudioManagerEvents audioManagerEvents) {
+    public void start() {
         Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()");
         ThreadUtils.checkIsOnMainThread();
         if (amState == AudioManagerState.RUNNING) {
             Log.e(Config.LOGTAG, "AudioManager is already active");
             return;
         }
-        awaitMicrophoneLatch();
-        this.audioManagerEvents = audioManagerEvents;
         amState = AudioManagerState.RUNNING;
         // Store current audio state so we can restore it when stop() is called.
         savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
         savedIsMicrophoneMute = audioManager.isMicrophoneMute();
         hasWiredHeadset = hasWiredHeadset();
         // Create an AudioManager.OnAudioFocusChangeListener instance.
-        audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
-            // Called on the listener to notify if the audio focus for this listener has been changed.
-            // The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
-            // and whether that loss is transient, or whether the new focus holder will hold it for an
-            // unknown amount of time.
-            // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains
-            // logging for now.
-            @Override
-            public void onAudioFocusChange(int focusChange) {
-                final String typeOfChange;
-                switch (focusChange) {
-                    case AudioManager.AUDIOFOCUS_GAIN:
-                        typeOfChange = "AUDIOFOCUS_GAIN";
-                        break;
-                    case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
-                        typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT";
-                        break;
-                    case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
-                        typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
-                        break;
-                    case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
-                        typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
-                        break;
-                    case AudioManager.AUDIOFOCUS_LOSS:
-                        typeOfChange = "AUDIOFOCUS_LOSS";
-                        break;
-                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
-                        typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT";
-                        break;
-                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
-                        typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
-                        break;
-                    default:
-                        typeOfChange = "AUDIOFOCUS_INVALID";
-                        break;
-                }
-                Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange);
-            }
-        };
+        audioFocusChangeListener =
+                new AudioManager.OnAudioFocusChangeListener() {
+                    // Called on the listener to notify if the audio focus for this listener has
+                    // been changed.
+                    // The |focusChange| value indicates whether the focus was gained, whether the
+                    // focus was lost,
+                    // and whether that loss is transient, or whether the new focus holder will hold
+                    // it for an
+                    // unknown amount of time.
+                    // TODO(henrika): possibly extend support of handling audio-focus changes. Only
+                    // contains
+                    // logging for now.
+                    @Override
+                    public void onAudioFocusChange(final int focusChange) {
+                        final String typeOfChange =
+                                switch (focusChange) {
+                                    case AudioManager.AUDIOFOCUS_GAIN -> "AUDIOFOCUS_GAIN";
+                                    case AudioManager
+                                            .AUDIOFOCUS_GAIN_TRANSIENT -> "AUDIOFOCUS_GAIN_TRANSIENT";
+                                    case AudioManager
+                                            .AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
+                                    case AudioManager
+                                            .AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
+                                    case AudioManager.AUDIOFOCUS_LOSS -> "AUDIOFOCUS_LOSS";
+                                    case AudioManager
+                                            .AUDIOFOCUS_LOSS_TRANSIENT -> "AUDIOFOCUS_LOSS_TRANSIENT";
+                                    case AudioManager
+                                            .AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
+                                    default -> "AUDIOFOCUS_INVALID";
+                                };
+                        Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange);
+                    }
+                };
         // Request audio playout focus (without ducking) and install listener for changes in focus.
-        int result = audioManager.requestAudioFocus(audioFocusChangeListener,
-                AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        int result =
+                audioManager.requestAudioFocus(
+                        audioFocusChangeListener,
+                        AudioManager.STREAM_VOICE_CALL,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
         if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
             Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
         } else {
@@ -258,8 +157,8 @@ public class AppRTCAudioManager {
         // Always disable microphone mute during a WebRTC call.
         setMicrophoneMute(false);
         // Set initial device states.
-        userSelectedAudioDevice = AudioDevice.NONE;
-        selectedAudioDevice = AudioDevice.NONE;
+        userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
+        selectedAudioDevice = CallIntegration.AudioDevice.NONE;
         audioDevices.clear();
         // Initialize and start Bluetooth if a BT device is available or initiate
         // detection of new (enabled) BT devices.
@@ -274,20 +173,9 @@ public class AppRTCAudioManager {
         Log.d(Config.LOGTAG, "AudioManager started");
     }
 
-    private void awaitMicrophoneLatch() {
-        final CountDownLatch latch = microphoneLatch;
-        if (latch == null) {
-            return;
-        }
-        try {
-            latch.await();
-        } catch (InterruptedException e) {
-            //ignore
-        }
-    }
-
     @SuppressWarnings("deprecation")
     public void stop() {
+        Log.d(Config.LOGTAG, "appRtpAudioManager.stop()");
         Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()");
         ThreadUtils.checkIsOnMainThread();
         if (amState != AudioManagerState.RUNNING) {
@@ -308,65 +196,45 @@ public class AppRTCAudioManager {
         // Abandon audio focus. Gives the previous focus owner, if any, focus.
         audioManager.abandonAudioFocus(audioFocusChangeListener);
         audioFocusChangeListener = null;
-        Log.d(Config.LOGTAG, "Abandoned audio focus for VOICE_CALL streams");
-        if (proximitySensor != null) {
-            proximitySensor.stop();
-            proximitySensor = null;
-        }
         audioManagerEvents = null;
+        Log.d(Config.LOGTAG, "appRtpAudioManager.stopped()");
     }
 
-    /**
-     * Changes selection of the currently active audio device.
-     */
-    private void setAudioDeviceInternal(AudioDevice device) {
+    /** Changes selection of the currently active audio device. */
+    private void setAudioDeviceInternal(final CallIntegration.AudioDevice device) {
         Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
         AppRTCUtils.assertIsTrue(audioDevices.contains(device));
         switch (device) {
-            case SPEAKER_PHONE:
-                setSpeakerphoneOn(true);
-                break;
-            case EARPIECE:
-            case WIRED_HEADSET:
-            case BLUETOOTH:
-                setSpeakerphoneOn(false);
-                break;
-            default:
-                Log.e(Config.LOGTAG, "Invalid audio device selection");
-                break;
+            case SPEAKER_PHONE -> setSpeakerphoneOn(true);
+            case EARPIECE, WIRED_HEADSET, BLUETOOTH -> setSpeakerphoneOn(false);
+            default -> Log.e(Config.LOGTAG, "Invalid audio device selection");
         }
         selectedAudioDevice = device;
     }
 
     /**
-     * Changes default audio device.
-     * TODO(henrika): add usage of this method in the AppRTCMobile client.
+     * Changes default audio device. TODO(henrika): add usage of this method in the AppRTCMobile
+     * client.
      */
-    public void setDefaultAudioDevice(AudioDevice defaultDevice) {
+    public void setDefaultAudioDevice(final CallIntegration.AudioDevice defaultDevice) {
         ThreadUtils.checkIsOnMainThread();
         switch (defaultDevice) {
-            case SPEAKER_PHONE:
-                defaultAudioDevice = defaultDevice;
-                break;
-            case EARPIECE:
+            case SPEAKER_PHONE -> defaultAudioDevice = defaultDevice;
+            case EARPIECE -> {
                 if (hasEarpiece()) {
                     defaultAudioDevice = defaultDevice;
                 } else {
-                    defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+                    defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
                 }
-                break;
-            default:
-                Log.e(Config.LOGTAG, "Invalid default audio device selection");
-                break;
+            }
+            default -> Log.e(Config.LOGTAG, "Invalid default audio device selection");
         }
         Log.d(Config.LOGTAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
         updateAudioDeviceState();
     }
 
-    /**
-     * Changes selection of the currently active audio device.
-     */
-    public void selectAudioDevice(AudioDevice device) {
+    /** Changes selection of the currently active audio device. */
+    public void selectAudioDevice(final CallIntegration.AudioDevice device) {
         ThreadUtils.checkIsOnMainThread();
         if (!audioDevices.contains(device)) {
             Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices);
@@ -375,39 +243,27 @@ public class AppRTCAudioManager {
         updateAudioDeviceState();
     }
 
-    /**
-     * Returns current set of available/selectable audio devices.
-     */
-    public Set<AudioDevice> getAudioDevices() {
-        ThreadUtils.checkIsOnMainThread();
-        return Collections.unmodifiableSet(new HashSet<>(audioDevices));
+    /** Returns current set of available/selectable audio devices. */
+    public Set<CallIntegration.AudioDevice> getAudioDevices() {
+        return ImmutableSet.copyOf(audioDevices);
     }
 
-    /**
-     * Returns the currently selected audio device.
-     */
-    public AudioDevice getSelectedAudioDevice() {
-        ThreadUtils.checkIsOnMainThread();
+    /** Returns the currently selected audio device. */
+    public CallIntegration.AudioDevice getSelectedAudioDevice() {
         return selectedAudioDevice;
     }
 
-    /**
-     * Helper method for receiver registration.
-     */
+    /** Helper method for receiver registration. */
     private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
         apprtcContext.registerReceiver(receiver, filter);
     }
 
-    /**
-     * Helper method for unregistration of an existing receiver.
-     */
+    /** Helper method for unregistration of an existing receiver. */
     private void unregisterReceiver(BroadcastReceiver receiver) {
         apprtcContext.unregisterReceiver(receiver);
     }
 
-    /**
-     * Sets the speaker phone mode.
-     */
+    /** Sets the speaker phone mode. */
     private void setSpeakerphoneOn(boolean on) {
         boolean wasOn = audioManager.isSpeakerphoneOn();
         if (wasOn == on) {
@@ -416,9 +272,7 @@ public class AppRTCAudioManager {
         audioManager.setSpeakerphoneOn(on);
     }
 
-    /**
-     * Sets the microphone mute state.
-     */
+    /** Sets the microphone mute state. */
     private void setMicrophoneMute(boolean on) {
         boolean wasMuted = audioManager.isMicrophoneMute();
         if (wasMuted == on) {
@@ -427,53 +281,57 @@ public class AppRTCAudioManager {
         audioManager.setMicrophoneMute(on);
     }
 
-    /**
-     * Gets the current earpiece state.
-     */
+    /** Gets the current earpiece state. */
     private boolean hasEarpiece() {
         return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
     }
 
     /**
-     * Checks whether a wired headset is connected or not.
-     * This is not a valid indication that audio playback is actually over
-     * the wired headset as audio routing depends on other conditions. We
-     * only use it as an early indicator (during initialization) of an attached
-     * wired headset.
+     * Checks whether a wired headset is connected or not. This is not a valid indication that audio
+     * playback is actually over the wired headset as audio routing depends on other conditions. We
+     * only use it as an early indicator (during initialization) of an attached wired headset.
      */
     @Deprecated
     private boolean hasWiredHeadset() {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            return audioManager.isWiredHeadsetOn();
-        } else {
-            final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
-            for (AudioDeviceInfo device : devices) {
-                final int type = device.getType();
-                if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
-                    Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset");
-                    return true;
-                } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
-                    Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device");
-                    return true;
-                }
+        final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+        for (AudioDeviceInfo device : devices) {
+            final int type = device.getType();
+            if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
+                Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset");
+                return true;
+            } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
+                Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device");
+                return true;
             }
-            return false;
         }
+        return false;
     }
 
     /**
-     * Updates list of possible audio devices and make new device selection.
-     * TODO(henrika): add unit test to verify all state transitions.
+     * Updates list of possible audio devices and make new device selection. TODO(henrika): add unit
+     * test to verify all state transitions.
      */
     public void updateAudioDeviceState() {
         ThreadUtils.checkIsOnMainThread();
-        Log.d(Config.LOGTAG, "--- updateAudioDeviceState: "
-                + "wired headset=" + hasWiredHeadset + ", "
-                + "BT state=" + bluetoothManager.getState());
-        Log.d(Config.LOGTAG, "Device status: "
-                + "available=" + audioDevices + ", "
-                + "selected=" + selectedAudioDevice + ", "
-                + "user selected=" + userSelectedAudioDevice);
+        Log.d(
+                Config.LOGTAG,
+                "--- updateAudioDeviceState: "
+                        + "wired headset="
+                        + hasWiredHeadset
+                        + ", "
+                        + "BT state="
+                        + bluetoothManager.getState());
+        Log.d(
+                Config.LOGTAG,
+                "Device status: "
+                        + "available="
+                        + audioDevices
+                        + ", "
+                        + "selected="
+                        + selectedAudioDevice
+                        + ", "
+                        + "user selected="
+                        + userSelectedAudioDevice);
         // Check if any Bluetooth headset is connected. The internal BT state will
         // change accordingly.
         // TODO(henrika): perhaps wrap required state into BT manager.
@@ -483,21 +341,21 @@ public class AppRTCAudioManager {
             bluetoothManager.updateDevice();
         }
         // Update the set of available audio devices.
-        Set<AudioDevice> newAudioDevices = new HashSet<>();
+        Set<CallIntegration.AudioDevice> newAudioDevices = new HashSet<>();
         if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
                 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
                 || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
-            newAudioDevices.add(AudioDevice.BLUETOOTH);
+            newAudioDevices.add(CallIntegration.AudioDevice.BLUETOOTH);
         }
         if (hasWiredHeadset) {
             // If a wired headset is connected, then it is the only possible option.
-            newAudioDevices.add(AudioDevice.WIRED_HEADSET);
+            newAudioDevices.add(CallIntegration.AudioDevice.WIRED_HEADSET);
         } else {
             // No wired headset, hence the audio-device list can contain speaker
             // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
-            newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
+            newAudioDevices.add(CallIntegration.AudioDevice.SPEAKER_PHONE);
             if (hasEarpiece()) {
-                newAudioDevices.add(AudioDevice.EARPIECE);
+                newAudioDevices.add(CallIntegration.AudioDevice.EARPIECE);
             }
         }
         // Store state which is set to true if the device list has changed.
@@ -506,39 +364,51 @@ public class AppRTCAudioManager {
         audioDevices = newAudioDevices;
         // Correct user selected audio devices if needed.
         if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
-                && userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
+                && userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH) {
             // If BT is not available, it can't be the user selection.
-            userSelectedAudioDevice = AudioDevice.NONE;
+            userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
         }
-        if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) {
+        if (hasWiredHeadset
+                && userSelectedAudioDevice == CallIntegration.AudioDevice.SPEAKER_PHONE) {
             // If user selected speaker phone, but then plugged wired headset then make
             // wired headset as user selected device.
-            userSelectedAudioDevice = AudioDevice.WIRED_HEADSET;
+            userSelectedAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
         }
-        if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
+        if (!hasWiredHeadset
+                && userSelectedAudioDevice == CallIntegration.AudioDevice.WIRED_HEADSET) {
             // If user selected wired headset, but then unplugged wired headset then make
             // speaker phone as user selected device.
-            userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
+            userSelectedAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
         }
         // Need to start Bluetooth if it is available and user either selected it explicitly or
         // user did not select any output device.
         boolean needBluetoothAudioStart =
                 bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
-                        && (userSelectedAudioDevice == AudioDevice.NONE
-                        || userSelectedAudioDevice == AudioDevice.BLUETOOTH);
+                        && (userSelectedAudioDevice == CallIntegration.AudioDevice.NONE
+                                || userSelectedAudioDevice
+                                        == CallIntegration.AudioDevice.BLUETOOTH);
         // Need to stop Bluetooth audio if user selected different device and
         // Bluetooth SCO connection is established or in the process.
         boolean needBluetoothAudioStop =
                 (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
-                        || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
-                        && (userSelectedAudioDevice != AudioDevice.NONE
-                        && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
+                                || bluetoothManager.getState()
+                                        == AppRTCBluetoothManager.State.SCO_CONNECTING)
+                        && (userSelectedAudioDevice != CallIntegration.AudioDevice.NONE
+                                && userSelectedAudioDevice
+                                        != CallIntegration.AudioDevice.BLUETOOTH);
         if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
                 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
                 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
-            Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
-                    + "stop=" + needBluetoothAudioStop + ", "
-                    + "BT state=" + bluetoothManager.getState());
+            Log.d(
+                    Config.LOGTAG,
+                    "Need BT audio: start="
+                            + needBluetoothAudioStart
+                            + ", "
+                            + "stop="
+                            + needBluetoothAudioStop
+                            + ", "
+                            + "BT state="
+                            + bluetoothManager.getState());
         }
         // Start or stop Bluetooth SCO connection given states set earlier.
         if (needBluetoothAudioStop) {
@@ -549,25 +419,26 @@ public class AppRTCAudioManager {
             // Attempt to start Bluetooth SCO audio (takes a few second to start).
             if (!bluetoothManager.startScoAudio()) {
                 // Remove BLUETOOTH from list of available devices since SCO failed.
-                audioDevices.remove(AudioDevice.BLUETOOTH);
+                audioDevices.remove(CallIntegration.AudioDevice.BLUETOOTH);
                 audioDeviceSetUpdated = true;
             }
         }
         // Update selected audio device.
-        final AudioDevice newAudioDevice;
+        final CallIntegration.AudioDevice newAudioDevice;
         if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
             // If a Bluetooth is connected, then it should be used as output audio
             // device. Note that it is not sufficient that a headset is available;
             // an active SCO channel must also be up and running.
-            newAudioDevice = AudioDevice.BLUETOOTH;
+            newAudioDevice = CallIntegration.AudioDevice.BLUETOOTH;
         } else if (hasWiredHeadset) {
             // If a wired headset is connected, but Bluetooth is not, then wired headset is used as
             // audio device.
-            newAudioDevice = AudioDevice.WIRED_HEADSET;
+            newAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
         } else {
             // No wired headset and no Bluetooth, hence the audio-device list can contain speaker
             // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
-            // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
+            // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or
+            // AudioDevice.EARPIECE
             // depending on the user's selection.
             newAudioDevice = defaultAudioDevice;
         }
@@ -575,9 +446,14 @@ public class AppRTCAudioManager {
         if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
             // Do the required device switch.
             setAudioDeviceInternal(newAudioDevice);
-            Log.d(Config.LOGTAG, "New device status: "
-                    + "available=" + audioDevices + ", "
-                    + "selected=" + newAudioDevice);
+            Log.d(
+                    Config.LOGTAG,
+                    "New device status: "
+                            + "available="
+                            + audioDevices
+                            + ", "
+                            + "selected="
+                            + newAudioDevice);
             if (audioManagerEvents != null) {
                 // Notify a listening client that audio device has been changed.
                 audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
@@ -586,40 +462,46 @@ public class AppRTCAudioManager {
         Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
     }
 
-    /**
-     * AudioDevice is the names of possible audio devices that we currently
-     * support.
-     */
-    public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE}
+    public void executeOnMain(final Runnable runnable) {
+        ContextCompat.getMainExecutor(apprtcContext).execute(runnable);
+    }
 
-    /**
-     * AudioManager state.
-     */
+    public void startRingBack() {
+        this.ringBackFuture =
+                JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
+                        () -> {
+                            final var toneGenerator =
+                                    new ToneGenerator(
+                                            AudioManager.STREAM_MUSIC,
+                                            CallIntegration.DEFAULT_TONE_VOLUME);
+                            toneGenerator.startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
+                        },
+                        0,
+                        3,
+                        TimeUnit.SECONDS);
+    }
+
+    public void stopRingBack() {
+        final var future = this.ringBackFuture;
+        if (future == null || future.isDone()) {
+            return;
+        }
+        future.cancel(true);
+    }
+
+    /** AudioManager state. */
     public enum AudioManagerState {
         UNINITIALIZED,
         PREINITIALIZED,
         RUNNING,
     }
 
-    public enum SpeakerPhonePreference {
-        AUTO, EARPIECE, SPEAKER;
-
-        public static SpeakerPhonePreference of(final Set<Media> media) {
-            if (media.contains(Media.VIDEO)) {
-                return SPEAKER;
-            } else {
-                return EARPIECE;
-            }
-        }
-    }
-
-    /**
-     * Selected audio device change event.
-     */
+    /** Selected audio device change event. */
     public interface AudioManagerEvents {
         // Callback fired once audio device is changed or list of available audio devices changed.
         void onAudioDeviceChanged(
-                AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
+                CallIntegration.AudioDevice selectedAudioDevice,
+                Set<CallIntegration.AudioDevice> availableAudioDevices);
     }
 
     /* Receiver which handles changes in wired headset availability. */
@@ -634,13 +516,23 @@ public class AppRTCAudioManager {
             int state = intent.getIntExtra("state", STATE_UNPLUGGED);
             int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
             String name = intent.getStringExtra("name");
-            Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": "
-                    + "a=" + intent.getAction() + ", s="
-                    + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m="
-                    + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb="
-                    + isInitialStickyBroadcast());
+            Log.d(
+                    Config.LOGTAG,
+                    "WiredHeadsetReceiver.onReceive"
+                            + AppRTCUtils.getThreadInfo()
+                            + ": "
+                            + "a="
+                            + intent.getAction()
+                            + ", s="
+                            + (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
+                            + ", m="
+                            + (microphone == HAS_MIC ? "mic" : "no mic")
+                            + ", n="
+                            + name
+                            + ", sb="
+                            + isInitialStickyBroadcast());
             hasWiredHeadset = (state == STATE_PLUGGED);
             updateAudioDeviceState();
         }
     }
-}
+}

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

@@ -68,8 +68,6 @@ public class AppRTCBluetoothManager {
             };
 
     protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
-        Log.d(Config.LOGTAG, "ctor");
-        ThreadUtils.checkIsOnMainThread();
         apprtcContext = context;
         apprtcAudioManager = audioManager;
         this.audioManager = getAudioManager(context);

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

@@ -1,171 +0,0 @@
-/*
- *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
- *
- *  Use of this source code is governed by a BSD-style license
- *  that can be found in the LICENSE file in the root of the source
- *  tree. An additional intellectual property rights grant can be found
- *  in the file PATENTS.  All contributing project authors may
- *  be found in the AUTHORS file in the root of the source tree.
- */
-package eu.siacs.conversations.services;
-
-import android.content.Context;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import org.webrtc.ThreadUtils;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.utils.AppRTCUtils;
-
-/**
- * AppRTCProximitySensor manages functions related to the proximity sensor in
- * the AppRTC demo.
- * On most device, the proximity sensor is implemented as a boolean-sensor.
- * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX
- * value i.e. the LUX value of the light sensor is compared with a threshold.
- * A LUX-value more than the threshold means the proximity sensor returns "FAR".
- * Anything less than the threshold value and the sensor  returns "NEAR".
- */
-public class AppRTCProximitySensor implements SensorEventListener {
-    // This class should be created, started and stopped on one thread
-    // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is
-    // the case. Only active when |DEBUG| is set to true.
-    private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
-    private final Runnable onSensorStateListener;
-    private final SensorManager sensorManager;
-    @Nullable
-    private Sensor proximitySensor;
-    private boolean lastStateReportIsNear;
-
-    private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
-        Log.d(Config.LOGTAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo());
-        onSensorStateListener = sensorStateListener;
-        sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
-    }
-
-    /**
-     * Construction
-     */
-    static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) {
-        return new AppRTCProximitySensor(context, sensorStateListener);
-    }
-
-    /**
-     * Activate the proximity sensor. Also do initialization if called for the
-     * first time.
-     */
-    public boolean start() {
-        threadChecker.checkIsOnValidThread();
-        Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo());
-        if (!initDefaultSensor()) {
-            // Proximity sensor is not supported on this device.
-            return false;
-        }
-        sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
-        return true;
-    }
-
-    /**
-     * Deactivate the proximity sensor.
-     */
-    public void stop() {
-        threadChecker.checkIsOnValidThread();
-        Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo());
-        if (proximitySensor == null) {
-            return;
-        }
-        sensorManager.unregisterListener(this, proximitySensor);
-    }
-
-    /**
-     * Getter for last reported state. Set to true if "near" is reported.
-     */
-    public boolean sensorReportsNearState() {
-        threadChecker.checkIsOnValidThread();
-        return lastStateReportIsNear;
-    }
-
-    @Override
-    public final void onAccuracyChanged(Sensor sensor, int accuracy) {
-        threadChecker.checkIsOnValidThread();
-        AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY);
-        if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
-            Log.e(Config.LOGTAG, "The values returned by this sensor cannot be trusted");
-        }
-    }
-
-    @Override
-    public final void onSensorChanged(SensorEvent event) {
-        threadChecker.checkIsOnValidThread();
-        AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY);
-        // As a best practice; do as little as possible within this method and
-        // avoid blocking.
-        float distanceInCentimeters = event.values[0];
-        if (distanceInCentimeters < proximitySensor.getMaximumRange()) {
-            Log.d(Config.LOGTAG, "Proximity sensor => NEAR state");
-            lastStateReportIsNear = true;
-        } else {
-            Log.d(Config.LOGTAG, "Proximity sensor => FAR state");
-            lastStateReportIsNear = false;
-        }
-        // Report about new state to listening client. Client can then call
-        // sensorReportsNearState() to query the current state (NEAR or FAR).
-        if (onSensorStateListener != null) {
-            onSensorStateListener.run();
-        }
-        Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": "
-                + "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance="
-                + event.values[0]);
-    }
-
-    /**
-     * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7)
-     * does not support this type of sensor and false will be returned in such
-     * cases.
-     */
-    private boolean initDefaultSensor() {
-        if (proximitySensor != null) {
-            return true;
-        }
-        proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
-        if (proximitySensor == null) {
-            return false;
-        }
-        logProximitySensorInfo();
-        return true;
-    }
-
-    /**
-     * Helper method for logging information about the proximity sensor.
-     */
-    private void logProximitySensorInfo() {
-        if (proximitySensor == null) {
-            return;
-        }
-        StringBuilder info = new StringBuilder("Proximity sensor: ");
-        info.append("name=").append(proximitySensor.getName());
-        info.append(", vendor: ").append(proximitySensor.getVendor());
-        info.append(", power: ").append(proximitySensor.getPower());
-        info.append(", resolution: ").append(proximitySensor.getResolution());
-        info.append(", max range: ").append(proximitySensor.getMaximumRange());
-        info.append(", min delay: ").append(proximitySensor.getMinDelay());
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
-            // Added in API level 20.
-            info.append(", type: ").append(proximitySensor.getStringType());
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            // Added in API level 21.
-            info.append(", max delay: ").append(proximitySensor.getMaxDelay());
-            info.append(", reporting mode: ").append(proximitySensor.getReportingMode());
-            info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor());
-        }
-        Log.d(Config.LOGTAG, info.toString());
-    }
-}

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

@@ -11,8 +11,6 @@ import androidx.annotation.NonNull;
 import com.otaliastudios.transcoder.Transcoder;
 import com.otaliastudios.transcoder.TranscoderListener;
 
-import org.jetbrains.annotations.NotNull;
-
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.util.Objects;
@@ -90,7 +88,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
 
     private void processAsVideo() throws FileNotFoundException {
         Log.d(Config.LOGTAG, "processing file as video");
-        mXmppConnectionService.startForcingForegroundNotification();
+        mXmppConnectionService.startOngoingVideoTranscodingForegroundNotification();
         mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4"));
         final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
         if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
@@ -99,19 +97,27 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
 
         final boolean highQuality = "720".equals(getVideoCompression());
 
-        final Future<Void> future = Transcoder.into(file.getAbsolutePath()).
+        final Future<Void> future;
+        try {
+            future = Transcoder.into(file.getAbsolutePath()).
                 addDataSource(mXmppConnectionService, uri)
                 .setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
                 .setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
                 .setListener(this)
                 .transcode();
+        } catch (final RuntimeException e) {
+            // transcode can already throw if there is an invalid file format or a platform bug
+            mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
+            processAsFile();
+            return;
+        }
         try {
             future.get();
-        } catch (InterruptedException e) {
+        } catch (final InterruptedException e) {
             throw new AssertionError(e);
-        } catch (ExecutionException e) {
+        } catch (final ExecutionException e) {
             if (e.getCause() instanceof Error) {
-                mXmppConnectionService.stopForcingForegroundNotification();
+                mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
                 processAsFile();
             } else {
                 Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
@@ -130,7 +136,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
 
     @Override
     public void onTranscodeCompleted(int successCode) {
-        mXmppConnectionService.stopForcingForegroundNotification();
+        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
         final File file = mXmppConnectionService.getFileBackend().getFile(message);
         long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
         Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
@@ -154,13 +160,13 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
 
     @Override
     public void onTranscodeCanceled() {
-        mXmppConnectionService.stopForcingForegroundNotification();
+        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
         processAsFile();
     }
 
     @Override
-    public void onTranscodeFailed(@NonNull @NotNull Throwable exception) {
-        mXmppConnectionService.stopForcingForegroundNotification();
+    public void onTranscodeFailed(@NonNull final Throwable exception) {
+        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
         Log.d(Config.LOGTAG, "video transcoding failed", exception);
         processAsFile();
     }

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

@@ -48,7 +48,11 @@ public class BarcodeProvider extends ContentProvider implements ServiceConnectio
 		return Uri.parse("content://" + packageId + AUTHORITY + "/" + account.getJid().asBareJid() + ".png");
 	}
 
-	public static Bitmap create2dBarcodeBitmap(String input, int size) {
+	public static Bitmap create2dBarcodeBitmap(final String input, final int size) {
+		return create2dBarcodeBitmap(input, size, Color.BLACK, Color.WHITE);
+	}
+
+	public static Bitmap create2dBarcodeBitmap(final String input, final int size, final int black, final int white) {
 		try {
 			final QRCodeWriter barcodeWriter = new QRCodeWriter();
 			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
@@ -61,14 +65,14 @@ public class BarcodeProvider extends ContentProvider implements ServiceConnectio
 			for (int y = 0; y < height; y++) {
 				final int offset = y * width;
 				for (int x = 0; x < width; x++) {
-					pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE;
+					pixels[offset + x] = result.get(x, y) ? black : white;
 				}
 			}
 			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
 			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
 			return bitmap;
 		} catch (final Exception e) {
-			e.printStackTrace();
+			Log.e(Config.LOGTAG,"could not generate QR code image",e);
 			return null;
 		}
 	}

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

@@ -0,0 +1,577 @@
+package eu.siacs.conversations.services;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.Build;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.util.MainThreadExecutor;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
+import eu.siacs.conversations.xmpp.jingle.Media;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class CallIntegration extends Connection {
+
+    private static final List<String> BROKEN_DEVICE_MODELS =
+            Arrays.asList(
+                    "OnePlus6", // OnePlus 6 (Android 8.1-11) Device is buggy and always starts the
+                                // operating system call screen even though we want to be self
+                                // managed
+                    "RMX1921", // Realme XT (Android 9-10) shows "Call not sent" dialog
+                    "RMX1971", // Realme 5 Pro (Android 9-11), show "Call not sent" dialog
+                    "RMX1973", // Realme 5 Pro (see above),
+                    "RMX2071", // Realme X50 Pro 5G (Call not sent)
+                    "RMX2075L1", // Realme X50 Pro 5G
+                    "RMX2076" // Realme X50 Pro 5G
+                    );
+
+    public static final int DEFAULT_TONE_VOLUME = 60;
+    private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90;
+
+    private final Context context;
+
+    private final AppRTCAudioManager appRTCAudioManager;
+    private AudioDevice initialAudioDevice = null;
+
+    private boolean isAudioRoutingRequested = false;
+    private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false);
+    private final AtomicBoolean delayedDestructionInitiated = new AtomicBoolean(false);
+    private final AtomicBoolean isDestroyed = new AtomicBoolean(false);
+
+    private List<CallEndpoint> availableEndpoints = Collections.emptyList();
+
+    private Callback callback = null;
+
+    public CallIntegration(final Context context) {
+        this.context = context.getApplicationContext();
+        if (selfManaged()) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
+            } else {
+                throw new AssertionError(
+                        "Trying to set connection properties on unsupported version");
+            }
+            this.appRTCAudioManager = null;
+        } else {
+            this.appRTCAudioManager = new AppRTCAudioManager(context);
+            this.appRTCAudioManager.setAudioManagerEvents(this::onAudioDeviceChanged);
+        }
+        setRingbackRequested(true);
+    }
+
+    public void setCallback(final Callback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    public void onShowIncomingCallUi() {
+        Log.d(Config.LOGTAG, "onShowIncomingCallUi");
+        this.callback.onCallIntegrationShowIncomingCallUi();
+    }
+
+    @Override
+    public void onAnswer() {
+        this.callback.onCallIntegrationAnswer();
+    }
+
+    @Override
+    public void onDisconnect() {
+        Log.d(Config.LOGTAG, "onDisconnect()");
+        this.callback.onCallIntegrationDisconnect();
+    }
+
+    @Override
+    public void onReject() {
+        this.callback.onCallIntegrationReject();
+    }
+
+    @Override
+    public void onReject(final String replyMessage) {
+        Log.d(Config.LOGTAG, "onReject(" + replyMessage + ")");
+        this.callback.onCallIntegrationReject();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Override
+    public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {
+        Log.d(Config.LOGTAG, "onAvailableCallEndpointsChanged(" + availableEndpoints + ")");
+        this.availableEndpoints = availableEndpoints;
+        this.onAudioDeviceChanged(
+                getAudioDeviceUpsideDownCake(getCurrentCallEndpoint()),
+                ImmutableSet.copyOf(
+                        Lists.transform(
+                                availableEndpoints,
+                                CallIntegration::getAudioDeviceUpsideDownCake)));
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Override
+    public void onCallEndpointChanged(@NonNull final CallEndpoint callEndpoint) {
+        Log.d(Config.LOGTAG, "onCallEndpointChanged()");
+        this.onAudioDeviceChanged(
+                getAudioDeviceUpsideDownCake(callEndpoint),
+                ImmutableSet.copyOf(
+                        Lists.transform(
+                                this.availableEndpoints,
+                                CallIntegration::getAudioDeviceUpsideDownCake)));
+    }
+
+    @Override
+    public void onCallAudioStateChanged(final CallAudioState state) {
+        if (selfManaged() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            Log.d(Config.LOGTAG, "ignoring onCallAudioStateChange() on Upside Down Cake");
+            return;
+        }
+        Log.d(Config.LOGTAG, "onCallAudioStateChange(" + state + ")");
+        this.onAudioDeviceChanged(getAudioDeviceOreo(state), getAudioDevicesOreo(state));
+    }
+
+    public Set<AudioDevice> getAudioDevices() {
+        if (notSelfManaged(context)) {
+            return getAudioDevicesFallback();
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            return getAudioDevicesUpsideDownCake();
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            return getAudioDevicesOreo();
+        } else {
+            throw new AssertionError("Trying to get audio devices on unsupported version");
+        }
+    }
+
+    public AudioDevice getSelectedAudioDevice() {
+        if (notSelfManaged(context)) {
+            return getAudioDeviceFallback();
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            return getAudioDeviceUpsideDownCake();
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            return getAudioDeviceOreo();
+        } else {
+            throw new AssertionError("Trying to get selected audio device on unsupported version");
+        }
+    }
+
+    public void setAudioDevice(final AudioDevice audioDevice) {
+        if (notSelfManaged(context)) {
+            setAudioDeviceFallback(audioDevice);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            setAudioDeviceUpsideDownCake(audioDevice);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            setAudioDeviceOreo(audioDevice);
+        } else {
+            throw new AssertionError("Trying to set audio devices on unsupported version");
+        }
+    }
+
+    public void setAudioDeviceWhenAvailable(final AudioDevice audioDevice) {
+        final var available = getAudioDevices();
+        if (available.contains(audioDevice) && !available.contains(AudioDevice.BLUETOOTH)) {
+            this.setAudioDevice(audioDevice);
+        } else {
+            Log.d(
+                    Config.LOGTAG,
+                    "application requested to switch to "
+                            + audioDevice
+                            + " but we won't because available devices are "
+                            + available);
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    private Set<AudioDevice> getAudioDevicesUpsideDownCake() {
+        return ImmutableSet.copyOf(
+                Lists.transform(
+                        this.availableEndpoints, CallIntegration::getAudioDeviceUpsideDownCake));
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    private AudioDevice getAudioDeviceUpsideDownCake() {
+        return getAudioDeviceUpsideDownCake(getCurrentCallEndpoint());
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    private static AudioDevice getAudioDeviceUpsideDownCake(final CallEndpoint callEndpoint) {
+        if (callEndpoint == null) {
+            return AudioDevice.NONE;
+        }
+        final var endpointType = callEndpoint.getEndpointType();
+        return switch (endpointType) {
+            case CallEndpoint.TYPE_BLUETOOTH -> AudioDevice.BLUETOOTH;
+            case CallEndpoint.TYPE_EARPIECE -> AudioDevice.EARPIECE;
+            case CallEndpoint.TYPE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
+            case CallEndpoint.TYPE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
+            case CallEndpoint.TYPE_STREAMING -> AudioDevice.STREAMING;
+            case CallEndpoint.TYPE_UNKNOWN -> AudioDevice.NONE;
+            default -> throw new IllegalStateException("Unknown endpoint type " + endpointType);
+        };
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    private void setAudioDeviceUpsideDownCake(final AudioDevice audioDevice) {
+        final var callEndpointOptional =
+                Iterables.tryFind(
+                        this.availableEndpoints,
+                        e -> getAudioDeviceUpsideDownCake(e) == audioDevice);
+        if (callEndpointOptional.isPresent()) {
+            final var endpoint = callEndpointOptional.get();
+            requestCallEndpointChange(
+                    endpoint,
+                    MainThreadExecutor.getInstance(),
+                    result -> Log.d(Config.LOGTAG, "switched to endpoint " + endpoint));
+        } else {
+            Log.w(Config.LOGTAG, "no endpoint found matching " + audioDevice);
+        }
+    }
+
+    private Set<AudioDevice> getAudioDevicesOreo() {
+        final var audioState = getCallAudioState();
+        if (audioState == null) {
+            Log.d(
+                    Config.LOGTAG,
+                    "no CallAudioState available. returning empty set for audio devices");
+            return Collections.emptySet();
+        }
+        return getAudioDevicesOreo(audioState);
+    }
+
+    private static Set<AudioDevice> getAudioDevicesOreo(final CallAudioState callAudioState) {
+        final ImmutableSet.Builder<AudioDevice> supportedAudioDevicesBuilder =
+                new ImmutableSet.Builder<>();
+        final var supportedRouteMask = callAudioState.getSupportedRouteMask();
+        if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
+                == CallAudioState.ROUTE_BLUETOOTH) {
+            supportedAudioDevicesBuilder.add(AudioDevice.BLUETOOTH);
+        }
+        if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
+            supportedAudioDevicesBuilder.add(AudioDevice.EARPIECE);
+        }
+        if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
+            supportedAudioDevicesBuilder.add(AudioDevice.SPEAKER_PHONE);
+        }
+        if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
+                == CallAudioState.ROUTE_WIRED_HEADSET) {
+            supportedAudioDevicesBuilder.add(AudioDevice.WIRED_HEADSET);
+        }
+        return supportedAudioDevicesBuilder.build();
+    }
+
+    private AudioDevice getAudioDeviceOreo() {
+        final var audioState = getCallAudioState();
+        if (audioState == null) {
+            Log.d(Config.LOGTAG, "no CallAudioState available. returning NONE as audio device");
+            return AudioDevice.NONE;
+        }
+        return getAudioDeviceOreo(audioState);
+    }
+
+    private static AudioDevice getAudioDeviceOreo(final CallAudioState audioState) {
+        // technically we get a mask here; maybe we should query the mask instead
+        return switch (audioState.getRoute()) {
+            case CallAudioState.ROUTE_BLUETOOTH -> AudioDevice.BLUETOOTH;
+            case CallAudioState.ROUTE_EARPIECE -> AudioDevice.EARPIECE;
+            case CallAudioState.ROUTE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
+            case CallAudioState.ROUTE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
+            default -> AudioDevice.NONE;
+        };
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private void setAudioDeviceOreo(final AudioDevice audioDevice) {
+        switch (audioDevice) {
+            case EARPIECE -> setAudioRoute(CallAudioState.ROUTE_EARPIECE);
+            case BLUETOOTH -> setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
+            case WIRED_HEADSET -> setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
+            case SPEAKER_PHONE -> setAudioRoute(CallAudioState.ROUTE_SPEAKER);
+        }
+    }
+
+    private Set<AudioDevice> getAudioDevicesFallback() {
+        return requireAppRtcAudioManager().getAudioDevices();
+    }
+
+    private AudioDevice getAudioDeviceFallback() {
+        final var audioDevice = requireAppRtcAudioManager().getSelectedAudioDevice();
+        return audioDevice == null ? AudioDevice.NONE : audioDevice;
+    }
+
+    private void setAudioDeviceFallback(final AudioDevice audioDevice) {
+        final var audioManager = requireAppRtcAudioManager();
+        audioManager.executeOnMain(() -> audioManager.setDefaultAudioDevice(audioDevice));
+    }
+
+    @NonNull
+    private AppRTCAudioManager requireAppRtcAudioManager() {
+        if (this.appRTCAudioManager == null) {
+            throw new IllegalStateException(
+                    "You are trying to access the fallback audio manager on a modern device");
+        }
+        return this.appRTCAudioManager;
+    }
+
+    @Override
+    public void onSilence() {
+        this.callback.onCallIntegrationSilence();
+    }
+
+    @Override
+    public void onStateChanged(final int state) {
+        Log.d(Config.LOGTAG, "onStateChanged(" + state + ")");
+        if (notSelfManaged(context)) {
+            if (state == STATE_DIALING) {
+                requireAppRtcAudioManager().startRingBack();
+            } else {
+                requireAppRtcAudioManager().stopRingBack();
+            }
+        }
+        if (state == STATE_ACTIVE) {
+            playConnectedSound();
+        } else if (state == STATE_DISCONNECTED) {
+            final var audioManager = this.appRTCAudioManager;
+            if (audioManager != null) {
+                audioManager.executeOnMain(audioManager::stop);
+            }
+        }
+    }
+
+    private void playConnectedSound() {
+        final var audioAttributes =
+                new AudioAttributes.Builder()
+                        .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
+                        .build();
+        final var mediaPlayer =
+                MediaPlayer.create(
+                        context,
+                        R.raw.connected,
+                        audioAttributes,
+                        AudioManager.AUDIO_SESSION_ID_GENERATE);
+        mediaPlayer.setVolume(
+                DEFAULT_MEDIA_PLAYER_VOLUME / 100f, DEFAULT_MEDIA_PLAYER_VOLUME / 100f);
+        mediaPlayer.start();
+    }
+
+    public void success() {
+        Log.d(Config.LOGTAG, "CallIntegration.success()");
+        final var toneGenerator =
+                new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
+        toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
+        this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375);
+    }
+
+    public void accepted() {
+        Log.d(Config.LOGTAG, "CallIntegration.accepted()");
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+            this.destroyWith(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE, null));
+        } else {
+            this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
+        }
+    }
+
+    public void error() {
+        Log.d(Config.LOGTAG, "CallIntegration.error()");
+        final var toneGenerator =
+                new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
+        toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
+        this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375);
+    }
+
+    public void retracted() {
+        Log.d(Config.LOGTAG, "CallIntegration.retracted()");
+        // an alternative cause would be LOCAL
+        this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
+    }
+
+    public void rejected() {
+        Log.d(Config.LOGTAG, "CallIntegration.rejected()");
+        this.destroyWith(new DisconnectCause(DisconnectCause.REJECTED, null));
+    }
+
+    public void busy() {
+        Log.d(Config.LOGTAG, "CallIntegration.busy()");
+        final var toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80);
+        toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
+        this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500);
+    }
+
+    private void destroyWithDelay(final DisconnectCause disconnectCause, final int delay) {
+        if (this.delayedDestructionInitiated.compareAndSet(false, true)) {
+            JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
+                    () -> {
+                        this.setDisconnected(disconnectCause);
+                        this.destroyCallIntegration();
+                    },
+                    delay,
+                    TimeUnit.MILLISECONDS);
+        } else {
+            Log.w(Config.LOGTAG, "CallIntegration destruction has already been scheduled!");
+        }
+    }
+
+    private void destroyWith(final DisconnectCause disconnectCause) {
+        if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
+            Log.d(Config.LOGTAG, "CallIntegration has already been destroyed");
+            return;
+        }
+        this.setDisconnected(disconnectCause);
+        this.destroyCallIntegration();
+        Log.d(Config.LOGTAG, "destroyed!");
+    }
+
+    public static Uri address(final Jid contact) {
+        return Uri.parse(String.format("xmpp:%s", contact.toEscapedString()));
+    }
+
+    public void verifyDisconnected() {
+        if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
+            return;
+        }
+        throw new AssertionError("CallIntegration has not been disconnected");
+    }
+
+    private void onAudioDeviceChanged(
+            final CallIntegration.AudioDevice selectedAudioDevice,
+            final Set<CallIntegration.AudioDevice> availableAudioDevices) {
+        if (isAudioRoutingRequested) {
+            configureInitialAudioDevice(availableAudioDevices);
+        }
+        final var callback = this.callback;
+        if (callback == null) {
+            return;
+        }
+        callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
+    }
+
+    private void configureInitialAudioDevice(final Set<AudioDevice> availableAudioDevices) {
+        final var initialAudioDevice = this.initialAudioDevice;
+        if (initialAudioDevice == null) {
+            Log.d(Config.LOGTAG, "skipping configureInitialAudioDevice()");
+            return;
+        }
+        final var target = this.initialAudioDevice;
+        if (this.initialAudioDeviceConfigured.compareAndSet(false, true)) {
+            if (availableAudioDevices.contains(target)
+                    && !availableAudioDevices.contains(AudioDevice.BLUETOOTH)) {
+                setAudioDevice(target);
+                Log.d(Config.LOGTAG, "configured initial audio device: " + target);
+            } else {
+                Log.d(
+                        Config.LOGTAG,
+                        "not setting initial audio device. available devices: "
+                                + availableAudioDevices);
+            }
+        }
+    }
+
+    private boolean selfManaged() {
+        return selfManaged(context);
+    }
+
+    public static boolean selfManaged(final Context context) {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+                && hasSystemFeature(context)
+                && !BROKEN_DEVICE_MODELS.contains(Build.DEVICE);
+    }
+
+    public static boolean hasSystemFeature(final Context context) {
+        final var packageManager = context.getPackageManager();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            return packageManager.hasSystemFeature(PackageManager.FEATURE_TELECOM);
+        } else {
+            //noinspection deprecation
+            return packageManager.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE);
+        }
+    }
+
+    public static boolean notSelfManaged(final Context context) {
+        return !selfManaged(context);
+    }
+
+    public void setInitialAudioDevice(final AudioDevice audioDevice) {
+        Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")");
+        this.initialAudioDevice = audioDevice;
+    }
+
+    public void startAudioRouting() {
+        this.isAudioRoutingRequested = true;
+        if (selfManaged()) {
+            final var devices = getAudioDevices();
+            if (devices.isEmpty()) {
+                return;
+            }
+            configureInitialAudioDevice(devices);
+            return;
+        }
+        final var audioManager = requireAppRtcAudioManager();
+        audioManager.executeOnMain(
+                () -> {
+                    audioManager.start();
+                    this.onAudioDeviceChanged(
+                            audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices());
+                });
+    }
+
+    private void destroyCallIntegration() {
+        super.destroy();
+        this.isDestroyed.set(true);
+    }
+
+    public boolean isDestroyed() {
+        return this.isDestroyed.get();
+    }
+
+    /** AudioDevice is the names of possible audio devices that we currently support. */
+    public enum AudioDevice {
+        NONE,
+        SPEAKER_PHONE,
+        WIRED_HEADSET,
+        EARPIECE,
+        BLUETOOTH,
+        STREAMING
+    }
+
+    public static AudioDevice initialAudioDevice(final Set<Media> media) {
+        if (Media.audioOnly(media)) {
+            return AudioDevice.EARPIECE;
+        } else {
+            return AudioDevice.SPEAKER_PHONE;
+        }
+    }
+
+    public interface Callback {
+        void onCallIntegrationShowIncomingCallUi();
+
+        void onCallIntegrationDisconnect();
+
+        void onAudioDeviceChanged(
+                CallIntegration.AudioDevice selectedAudioDevice,
+                Set<CallIntegration.AudioDevice> availableAudioDevices);
+
+        void onCallIntegrationReject();
+
+        void onCallIntegrationAnswer();
+
+        void onCallIntegrationSilence();
+    }
+}

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

@@ -0,0 +1,462 @@
+package eu.siacs.conversations.services;
+
+import android.Manifest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.ui.RtpSessionActivity;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
+import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
+import eu.siacs.conversations.xmpp.jingle.Media;
+import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class CallIntegrationConnectionService extends ConnectionService {
+
+    private static final String EXTRA_ADDRESS = "eu.siacs.conversations.address";
+    private static final String EXTRA_SESSION_ID = "eu.siacs.conversations.sid";
+
+    private static final ExecutorService ACCOUNT_REGISTRATION_EXECUTOR =
+            Executors.newSingleThreadExecutor();
+
+    private ListenableFuture<ServiceConnectionService> serviceFuture;
+
+    @Override
+    public void onCreate() {
+        Log.d(Config.LOGTAG, "CallIntegrationService.onCreate()");
+        super.onCreate();
+        this.serviceFuture = ServiceConnectionService.bindService(this);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(Config.LOGTAG, "destroying CallIntegrationConnectionService");
+        super.onDestroy();
+        final ServiceConnection serviceConnection;
+        try {
+            serviceConnection = serviceFuture.get().serviceConnection;
+        } catch (final Exception e) {
+            Log.d(Config.LOGTAG, "could not fetch service connection", e);
+            return;
+        }
+        this.unbindService(serviceConnection);
+    }
+
+    private static Connection createOutgoingRtpConnection(
+            final XmppConnectionService service,
+            final String phoneAccountHandle,
+            final Jid with,
+            final Set<Media> media) {
+        if (service == null) {
+            Log.d(
+                    Config.LOGTAG,
+                    "CallIntegrationConnection service was unable to bind to XmppConnectionService");
+            return Connection.createFailedConnection(
+                    new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
+        }
+        final var account = service.findAccountByUuid(phoneAccountHandle);
+        return createOutgoingRtpConnection(service, account, with, media);
+    }
+
+    private static Connection createOutgoingRtpConnection(
+            @NonNull final XmppConnectionService service,
+            @NonNull final Account account,
+            final Jid with,
+            final Set<Media> media) {
+        Log.d(Config.LOGTAG, "create outgoing rtp connection!");
+        final Intent intent = new Intent(service, RtpSessionActivity.class);
+        intent.setAction(Intent.ACTION_VIEW);
+        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
+        intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        final Connection callIntegration;
+        if (with.isBareJid()) {
+            final var contact = account.getRoster().getContact(with);
+            if (Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK
+                    && contact.getPresences().isEmpty()) {
+                intent.putExtra(
+                        RtpSessionActivity.EXTRA_LAST_REPORTED_STATE,
+                        RtpEndUserState.CONTACT_OFFLINE.toString());
+                callIntegration =
+                        Connection.createFailedConnection(
+                                new DisconnectCause(DisconnectCause.ERROR, "contact is offline"));
+                // we can use a JMI 'finish' message to notify the contact of a call we never
+                // actually attempted
+                // sendJingleFinishMessage(service, contact, Reason.CONNECTIVITY_ERROR);
+            } else {
+                final var proposal =
+                        service.getJingleConnectionManager()
+                                .proposeJingleRtpSession(account, with, media);
+                if (proposal == null) {
+                    // TODO instead of just null checking try to get the sessionID
+                    return Connection.createFailedConnection(
+                            new DisconnectCause(
+                                    DisconnectCause.ERROR, "a call is already in progress"));
+                }
+                intent.putExtra(
+                        RtpSessionActivity.EXTRA_LAST_REPORTED_STATE,
+                        RtpEndUserState.FINDING_DEVICE.toString());
+                intent.putExtra(RtpSessionActivity.EXTRA_PROPOSED_SESSION_ID, proposal.sessionId);
+                callIntegration = proposal.getCallIntegration();
+            }
+            if (Media.audioOnly(media)) {
+                intent.putExtra(
+                        RtpSessionActivity.EXTRA_LAST_ACTION,
+                        RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
+            } else {
+                intent.putExtra(
+                        RtpSessionActivity.EXTRA_LAST_ACTION,
+                        RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
+            }
+        } else {
+            final JingleRtpConnection jingleRtpConnection =
+                    service.getJingleConnectionManager().initializeRtpSession(account, with, media);
+            final String sessionId = jingleRtpConnection.getId().sessionId;
+            intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
+            callIntegration = jingleRtpConnection.getCallIntegration();
+        }
+        service.startActivity(intent);
+        return callIntegration;
+    }
+
+    private static void sendJingleFinishMessage(
+            final XmppConnectionService service, final Contact contact, final Reason reason) {
+        service.getJingleConnectionManager()
+                .sendJingleMessageFinish(contact, UUID.randomUUID().toString(), reason);
+    }
+
+    @Override
+    public Connection onCreateOutgoingConnection(
+            final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
+        Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")");
+        final var uri = request.getAddress();
+        final var extras = request.getExtras();
+        if (uri == null || !Arrays.asList("xmpp", "tel").contains(uri.getScheme())) {
+            return Connection.createFailedConnection(
+                    new DisconnectCause(DisconnectCause.ERROR, "invalid address"));
+        }
+        final Jid jid;
+        if ("tel".equals(uri.getScheme())) {
+            jid = Jid.ofEscaped(extras.getString(EXTRA_ADDRESS));
+        } else {
+            jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
+        }
+        final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE);
+        final Set<Media> media =
+                videoState == VideoProfile.STATE_AUDIO_ONLY
+                        ? ImmutableSet.of(Media.AUDIO)
+                        : ImmutableSet.of(Media.AUDIO, Media.VIDEO);
+        Log.d(Config.LOGTAG, "jid=" + jid);
+        Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId());
+        Log.d(Config.LOGTAG, "media " + media);
+        final var service = ServiceConnectionService.get(this.serviceFuture);
+        return createOutgoingRtpConnection(service, phoneAccountHandle.getId(), jid, media);
+    }
+
+    @Override
+    public Connection onCreateIncomingConnection(
+            final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
+        Log.d(Config.LOGTAG, "onCreateIncomingConnection()");
+        final var service = ServiceConnectionService.get(this.serviceFuture);
+        final Bundle extras = request.getExtras();
+        final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
+        final String incomingCallAddress =
+                extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
+        final String sid = extraExtras == null ? null : extraExtras.getString(EXTRA_SESSION_ID);
+        Log.d(Config.LOGTAG, "sid " + sid);
+        final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress);
+        Log.d(Config.LOGTAG, "uri=" + uri);
+        if (uri == null || sid == null) {
+            return Connection.createFailedConnection(
+                    new DisconnectCause(
+                            DisconnectCause.ERROR,
+                            "connection request is missing required information"));
+        }
+        if (service == null) {
+            return Connection.createFailedConnection(
+                    new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
+        }
+        final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
+        final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
+        final var weakReference =
+                service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid);
+        if (weakReference == null) {
+            Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid);
+            return Connection.createFailedConnection(
+                    new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found"));
+        }
+        final var jingleRtpConnection = weakReference.get();
+        if (jingleRtpConnection == null) {
+            Log.d(Config.LOGTAG, "connection has been terminated");
+            return Connection.createFailedConnection(
+                    new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated"));
+        }
+        Log.d(Config.LOGTAG, "registering call integration for incoming call");
+        return jingleRtpConnection.getCallIntegration();
+    }
+
+    public static void togglePhoneAccountAsync(final Context context, final Account account) {
+        ACCOUNT_REGISTRATION_EXECUTOR.execute(() -> togglePhoneAccount(context, account));
+    }
+
+    private static void togglePhoneAccount(final Context context, final Account account) {
+        if (account.isEnabled()) {
+            registerPhoneAccount(context, account);
+        } else {
+            unregisterPhoneAccount(context, account);
+        }
+    }
+
+    private static void registerPhoneAccount(final Context context, final Account account) {
+        try {
+            registerPhoneAccountOrThrow(context, account);
+        } catch (final IllegalArgumentException e) {
+            Log.w(
+                    Config.LOGTAG,
+                    "could not register phone account for " + account.getJid().asBareJid(),
+                    e);
+            ContextCompat.getMainExecutor(context)
+                    .execute(() -> showCallIntegrationNotAvailable(context));
+        }
+    }
+
+    private static void showCallIntegrationNotAvailable(final Context context) {
+        Toast.makeText(context, R.string.call_integration_not_available, Toast.LENGTH_LONG).show();
+    }
+
+    private static void registerPhoneAccountOrThrow(final Context context, final Account account) {
+        final var handle = getHandle(context, account);
+        final var telecomManager = context.getSystemService(TelecomManager.class);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            if (telecomManager.getOwnSelfManagedPhoneAccounts().contains(handle)) {
+                Log.d(
+                        Config.LOGTAG,
+                        "a phone account for " + account.getJid().asBareJid() + " already exists");
+                return;
+            }
+        }
+        final var builder =
+                PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid());
+        builder.setSupportedUriSchemes(Collections.singletonList("xmpp"));
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            builder.setCapabilities(
+                    PhoneAccount.CAPABILITY_SELF_MANAGED
+                            | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
+        }
+        final var phoneAccount = builder.build();
+        telecomManager.registerPhoneAccount(phoneAccount);
+    }
+
+    public static void togglePhoneAccountsAsync(
+            final Context context, final Collection<Account> accounts) {
+        ACCOUNT_REGISTRATION_EXECUTOR.execute(() -> togglePhoneAccounts(context, accounts));
+    }
+
+    private static void togglePhoneAccounts(
+            final Context context, final Collection<Account> accounts) {
+        for (final Account account : accounts) {
+            if (account.isEnabled()) {
+                try {
+                    registerPhoneAccountOrThrow(context, account);
+                } catch (final IllegalArgumentException e) {
+                    Log.w(
+                            Config.LOGTAG,
+                            "could not register phone account for " + account.getJid().asBareJid(),
+                            e);
+                }
+            } else {
+                unregisterPhoneAccount(context, account);
+            }
+        }
+    }
+
+    public static void unregisterPhoneAccount(final Context context, final Account account) {
+        final var handle = getHandle(context, account);
+        final var telecomManager = context.getSystemService(TelecomManager.class);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            if (telecomManager.getOwnSelfManagedPhoneAccounts().contains(handle)) {
+                telecomManager.unregisterPhoneAccount(handle);
+            }
+        } else {
+            telecomManager.unregisterPhoneAccount(handle);
+        }
+    }
+
+    public static PhoneAccountHandle getHandle(final Context context, final Account account) {
+        final var competentName =
+                new ComponentName(context, CallIntegrationConnectionService.class);
+        return new PhoneAccountHandle(competentName, account.getUuid());
+    }
+
+    public static void placeCall(
+            final XmppConnectionService service,
+            final Account account,
+            final Jid with,
+            final Set<Media> media) {
+        if (CallIntegration.selfManaged(service)) {
+            final var extras = new Bundle();
+            extras.putParcelable(
+                    TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(service, account));
+            extras.putInt(
+                    TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    Media.audioOnly(media)
+                            ? VideoProfile.STATE_AUDIO_ONLY
+                            : VideoProfile.STATE_BIDIRECTIONAL);
+            if (service.checkSelfPermission(Manifest.permission.MANAGE_OWN_CALLS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                Toast.makeText(service, R.string.no_permission_to_place_call, Toast.LENGTH_SHORT)
+                        .show();
+                return;
+            }
+            final Uri address;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                // Android 9+ supports putting xmpp uris into the address
+                address = CallIntegration.address(with);
+            } else {
+                // for Android 8 we need to put in a fake tel uri
+                final var outgoingCallExtras = new Bundle();
+                outgoingCallExtras.putString(EXTRA_ADDRESS, with.toEscapedString());
+                extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingCallExtras);
+                address = Uri.parse("tel:0");
+            }
+            try {
+                service.getSystemService(TelecomManager.class).placeCall(address, extras);
+            } catch (final SecurityException e) {
+                Log.e(Config.LOGTAG, "call integration not available", e);
+                Toast.makeText(service, R.string.call_integration_not_available, Toast.LENGTH_LONG)
+                        .show();
+            }
+        } else {
+            final var connection = createOutgoingRtpConnection(service, account, with, media);
+            if (connection != null) {
+                Log.d(
+                        Config.LOGTAG,
+                        "not adding outgoing call to TelecomManager on Android "
+                                + Build.VERSION.RELEASE
+                                + " ("
+                                + Build.DEVICE
+                                + ")");
+            }
+        }
+    }
+
+    public static boolean addNewIncomingCall(
+            final Context context, final AbstractJingleConnection.Id id) {
+        if (CallIntegration.notSelfManaged(context)) {
+            Log.d(
+                    Config.LOGTAG,
+                    "not adding incoming call to TelecomManager on Android "
+                            + Build.VERSION.RELEASE
+                            + " ("
+                            + Build.DEVICE
+                            + ")");
+            return true;
+        }
+        final var phoneAccountHandle =
+                CallIntegrationConnectionService.getHandle(context, id.account);
+        final var bundle = new Bundle();
+        bundle.putString(
+                TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+                CallIntegration.address(id.with).toString());
+        final var extras = new Bundle();
+        extras.putString(EXTRA_SESSION_ID, id.sessionId);
+        bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
+        try {
+            context.getSystemService(TelecomManager.class)
+                    .addNewIncomingCall(phoneAccountHandle, bundle);
+        } catch (final SecurityException e) {
+            Log.e(
+                    Config.LOGTAG,
+                    id.account.getJid().asBareJid() + ": call integration not available",
+                    e);
+            return false;
+        }
+        return true;
+    }
+
+    public static class ServiceConnectionService {
+        private final ServiceConnection serviceConnection;
+        private final XmppConnectionService service;
+
+        public ServiceConnectionService(
+                final ServiceConnection serviceConnection, final XmppConnectionService service) {
+            this.serviceConnection = serviceConnection;
+            this.service = service;
+        }
+
+        public static XmppConnectionService get(
+                final ListenableFuture<ServiceConnectionService> future) {
+            try {
+                return future.get(2, TimeUnit.SECONDS).service;
+            } catch (final ExecutionException | InterruptedException | TimeoutException e) {
+                return null;
+            }
+        }
+
+        public static ListenableFuture<ServiceConnectionService> bindService(
+                final Context context) {
+            final SettableFuture<ServiceConnectionService> serviceConnectionFuture =
+                    SettableFuture.create();
+            final var intent = new Intent(context, XmppConnectionService.class);
+            intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED);
+            final var serviceConnection =
+                    new ServiceConnection() {
+
+                        @Override
+                        public void onServiceConnected(
+                                final ComponentName name, final IBinder iBinder) {
+                            final XmppConnectionService.XmppConnectionBinder binder =
+                                    (XmppConnectionService.XmppConnectionBinder) iBinder;
+                            serviceConnectionFuture.set(
+                                    new ServiceConnectionService(this, binder.getService()));
+                        }
+
+                        @Override
+                        public void onServiceDisconnected(final ComponentName name) {}
+                    };
+            context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
+            return serviceConnectionFuture;
+        }
+    }
+}

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

@@ -1,5 +1,8 @@
 package eu.siacs.conversations.services;
 
+import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
+
+import android.os.Build;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -8,34 +11,45 @@ import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.TrustManagers;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Room;
 import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.http.services.MuclumbusService;
 import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.utils.TLSSocketFactory;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
 import okhttp3.OkHttpClient;
 import okhttp3.ResponseBody;
+
 import retrofit2.Call;
 import retrofit2.Callback;
 import retrofit2.Response;
 import retrofit2.Retrofit;
 import retrofit2.converter.gson.GsonConverterFactory;
 
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
 public class ChannelDiscoveryService {
 
     private final XmppConnectionService service;
@@ -55,6 +69,24 @@ public class ChannelDiscoveryService {
             return;
         }
         final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
+        try {
+            final X509TrustManager trustManager;
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
+                trustManager = TrustManagers.defaultWithBundledLetsEncrypt(service);
+            } else {
+                trustManager = TrustManagers.createDefaultTrustManager();
+            }
+            final SSLSocketFactory socketFactory =
+                    new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
+            builder.sslSocketFactory(socketFactory, trustManager);
+        } catch (final IOException
+                | KeyManagementException
+                | NoSuchAlgorithmException
+                | KeyStoreException
+                | CertificateException e) {
+            Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt");
+            throw new RuntimeException(e);
+        }
         if (service.useTorToConnect()) {
             builder.proxy(HttpConnectionManager.getProxy());
         }

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

@@ -324,7 +324,7 @@ public class ExportBackupService extends Service {
     private List<File> export(boolean withCheogramDb) throws Exception {
         NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
         mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
-                .setSmallIcon(R.drawable.ic_archive_white_24dp)
+                .setSmallIcon(R.drawable.ic_archive_24dp)
                 .setProgress(1, 0, false);
         startForeground(NOTIFICATION_ID, mBuilder.build());
         int count = 0;
@@ -432,10 +432,13 @@ public class ExportBackupService extends Service {
                 .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this).getAbsolutePath())))
                 .setAutoCancel(true)
                 .setContentIntent(openFolderIntent)
-                .setSmallIcon(R.drawable.ic_archive_white_24dp);
+                .setSmallIcon(R.drawable.ic_archive_24dp);
 
         if (shareFilesIntent != null) {
-            mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent);
+            mBuilder.addAction(
+                    R.drawable.ic_share_24dp,
+                    getString(R.string.share_backup_files),
+                    shareFilesIntent);
         }
 
         try { Thread.sleep(500); } catch (final Exception e) { }

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

@@ -181,7 +181,7 @@ public class MemorizingTrustManager {
         this.daneVerifier = new DaneVerifier();
         try {
             if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
-                this.defaultTrustManager = defaultWithBundledLetsEncrypt(context);
+                this.defaultTrustManager = TrustManagers.defaultWithBundledLetsEncrypt(context);
             } else {
                 this.defaultTrustManager = TrustManagers.createDefaultTrustManager();
             }
@@ -193,17 +193,6 @@ public class MemorizingTrustManager {
         }
     }
 
-    private static X509TrustManager defaultWithBundledLetsEncrypt(final Context context)
-            throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
-        final BundledTrustManager bundleTrustManager =
-                BundledTrustManager.builder()
-                        .loadKeyStore(
-                                context.getResources().openRawResource(R.raw.letsencrypt),
-                                "letsencrypt")
-                        .build();
-        return CombiningTrustManager.combineWithDefault(bundleTrustManager);
-    }
-
     private static boolean isIp(final String server) {
         return server != null
                 && (PATTERN_IPV4.matcher(server).matches()

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

@@ -4,7 +4,7 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
 import android.util.Log;
 
-import org.jetbrains.annotations.NotNull;
+import androidx.annotation.NonNull;
 
 import java.math.BigInteger;
 import java.util.ArrayList;
@@ -281,10 +281,18 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         if (conversation != null) {
             conversation.sort();
             conversation.setHasMessagesLeftOnServer(!done);
+            final var displayState = conversation.getDisplayState();
+            if (displayState != null) {
+                mXmppConnectionService.markReadUpToStanzaId(conversation, displayState);
+            }
         } else {
-            for (Conversation tmp : this.mXmppConnectionService.getConversations()) {
+            for (final Conversation tmp : this.mXmppConnectionService.getConversations()) {
                 if (tmp.getAccount() == query.getAccount()) {
                     tmp.sort();
+                    final var displayState = tmp.getDisplayState();
+                    if (displayState != null) {
+                        mXmppConnectionService.markReadUpToStanzaId(tmp, displayState);
+                    }
                 }
             }
         }
@@ -631,7 +639,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
             }
         }
 
-        @NotNull
+        @NonNull
         @Override
         public String toString() {
             StringBuilder builder = new StringBuilder();

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

@@ -3,6 +3,7 @@ package eu.siacs.conversations.services;
 import android.Manifest;
 import static eu.siacs.conversations.utils.Compatibility.s;
 
+import android.Manifest;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
@@ -17,13 +18,12 @@ import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Typeface;
 import android.media.AudioAttributes;
-import android.media.Ringtone;
+import android.media.AudioManager;
 import android.media.RingtoneManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.os.Vibrator;
 import android.preference.PreferenceManager;
 import android.provider.Settings;
 import android.telecom.PhoneAccountHandle;
@@ -34,7 +34,9 @@ import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.TypedValue;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.core.app.ActivityCompat;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationCompat.BigPictureStyle;
 import androidx.core.app.NotificationCompat.CallStyle;
@@ -47,29 +49,14 @@ import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.graphics.drawable.IconCompat;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
 
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
@@ -94,16 +81,29 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
 
-public class NotificationService {
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
-    private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
-            Executors.newSingleThreadScheduledExecutor();
+public class NotificationService {
 
     public static final Object CATCHUP_LOCK = new Object();
 
     private static final int LED_COLOR = 0xff7401cf;
 
-    private static final long[] CALL_PATTERN = {0, 500, 300, 600};
+    private static final long[] CALL_PATTERN = {0, 500, 300, 600, 3000};
 
     private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages";
     private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls";
@@ -115,6 +115,8 @@ public class NotificationService {
     public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10;
     public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
     private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13;
+    public static final int ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID =
+            NOTIFICATION_ID_MULTIPLIER * 14;
     private final XmppConnectionService mXmppConnectionService;
     private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
     private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
@@ -125,9 +127,9 @@ public class NotificationService {
     private long mLastNotification;
 
     private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
+    private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX =
+            "incoming_calls_channel#";
     private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
-    private Ringtone currentlyPlayingRingtone = null;
-    private ScheduledFuture<?> vibrationFuture;
 
     NotificationService(final XmppConnectionService service) {
         this.mXmppConnectionService = service;
@@ -168,6 +170,7 @@ public class NotificationService {
 
         notificationManager.deleteNotificationChannel("export");
         notificationManager.deleteNotificationChannel("incoming_calls");
+        notificationManager.deleteNotificationChannel(INCOMING_CALLS_NOTIFICATION_CHANNEL);
 
         notificationManager.createNotificationChannelGroup(
                 new NotificationChannelGroup(
@@ -218,19 +221,7 @@ public class NotificationService {
         exportChannel.setGroup("status");
         notificationManager.createNotificationChannel(exportChannel);
 
-        final NotificationChannel incomingCallsChannel =
-                new NotificationChannel(
-                        INCOMING_CALLS_NOTIFICATION_CHANNEL,
-                        c.getString(R.string.incoming_calls_channel_name),
-                        NotificationManager.IMPORTANCE_HIGH);
-        incomingCallsChannel.setSound(null, null);
-        incomingCallsChannel.setShowBadge(false);
-        incomingCallsChannel.setLightColor(LED_COLOR);
-        incomingCallsChannel.enableLights(true);
-        incomingCallsChannel.setGroup("calls");
-        incomingCallsChannel.setBypassDnd(true);
-        incomingCallsChannel.enableVibration(false);
-        notificationManager.createNotificationChannel(incomingCallsChannel);
+        createInitialIncomingCallChannelIfNecessary(c);
 
         final NotificationChannel ongoingCallsChannel =
                 new NotificationChannel(
@@ -263,7 +254,7 @@ public class NotificationService {
                 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
                 new AudioAttributes.Builder()
                         .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                        .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
+                        .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
                         .build());
         messagesChannel.setLightColor(LED_COLOR);
         final int dat = 70;
@@ -286,20 +277,6 @@ public class NotificationService {
         silentMessagesChannel.setGroup("chats");
         notificationManager.createNotificationChannel(silentMessagesChannel);
 
-        final NotificationChannel quietHoursChannel =
-                new NotificationChannel(
-                        "quiet_hours",
-                        c.getString(R.string.title_pref_quiet_hours),
-                        NotificationManager.IMPORTANCE_LOW);
-        quietHoursChannel.setShowBadge(true);
-        quietHoursChannel.setLightColor(LED_COLOR);
-        quietHoursChannel.enableLights(true);
-        quietHoursChannel.setGroup("chats");
-        quietHoursChannel.enableVibration(false);
-        quietHoursChannel.setSound(null, null);
-
-        notificationManager.createNotificationChannel(quietHoursChannel);
-
         final NotificationChannel deliveryFailedChannel =
                 new NotificationChannel(
                         "delivery_failed",
@@ -316,6 +293,98 @@ public class NotificationService {
         notificationManager.createNotificationChannel(deliveryFailedChannel);
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private static void createInitialIncomingCallChannelIfNecessary(final Context context) {
+        final var currentIteration = getCurrentIncomingCallChannelIteration(context);
+        if (currentIteration.isPresent()) {
+            return;
+        }
+        createInitialIncomingCallChannel(context);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    public static Optional<Integer> getCurrentIncomingCallChannelIteration(final Context context) {
+        final var notificationManager = context.getSystemService(NotificationManager.class);
+        for (final NotificationChannel channel : notificationManager.getNotificationChannels()) {
+            final String id = channel.getId();
+            if (Strings.isNullOrEmpty(id)) {
+                continue;
+            }
+            if (id.startsWith(INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX)) {
+                final var parts = Splitter.on('#').splitToList(id);
+                if (parts.size() == 2) {
+                    final var iteration = Ints.tryParse(parts.get(1));
+                    if (iteration != null) {
+                        return Optional.of(iteration);
+                    }
+                }
+            }
+        }
+        return Optional.absent();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    public static Optional<NotificationChannel> getCurrentIncomingCallChannel(
+            final Context context) {
+        final var iteration = getCurrentIncomingCallChannelIteration(context);
+        return iteration.transform(
+                i -> {
+                    final var notificationManager =
+                            context.getSystemService(NotificationManager.class);
+                    return notificationManager.getNotificationChannel(
+                            INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + i);
+                });
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private static void createInitialIncomingCallChannel(final Context context) {
+        final var appSettings = new AppSettings(context);
+        final var ringtoneUri = appSettings.getRingtone();
+        createIncomingCallChannel(context, ringtoneUri, 0);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    public static void recreateIncomingCallChannel(final Context context, final Uri ringtone) {
+        final var currentIteration = getCurrentIncomingCallChannelIteration(context);
+        final int nextIteration;
+        if (currentIteration.isPresent()) {
+            final var notificationManager = context.getSystemService(NotificationManager.class);
+            notificationManager.deleteNotificationChannel(
+                    INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + currentIteration.get());
+            nextIteration = currentIteration.get() + 1;
+        } else {
+            nextIteration = 0;
+        }
+        createIncomingCallChannel(context, ringtone, nextIteration);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private static void createIncomingCallChannel(
+            final Context context, final Uri ringtoneUri, final int iteration) {
+        final var notificationManager = context.getSystemService(NotificationManager.class);
+        final var id = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + iteration;
+        Log.d(Config.LOGTAG, "creating incoming call channel with id " + id);
+        final NotificationChannel incomingCallsChannel =
+                new NotificationChannel(
+                        id,
+                        context.getString(R.string.incoming_calls_channel_name),
+                        NotificationManager.IMPORTANCE_HIGH);
+        incomingCallsChannel.setSound(
+                ringtoneUri,
+                new AudioAttributes.Builder()
+                        .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                        .build());
+        incomingCallsChannel.setShowBadge(false);
+        incomingCallsChannel.setLightColor(LED_COLOR);
+        incomingCallsChannel.enableLights(true);
+        incomingCallsChannel.setGroup("calls");
+        incomingCallsChannel.setBypassDnd(true);
+        incomingCallsChannel.enableVibration(true);
+        incomingCallsChannel.setVibrationPattern(CALL_PATTERN);
+        notificationManager.createNotificationChannel(incomingCallsChannel);
+    }
+
     private boolean notifyMessage(final Message message) {
         final Conversation conversation = (Conversation) message.getConversation();
         return message.getStatus() == Message.STATUS_RECEIVED
@@ -482,7 +551,7 @@ public class NotificationService {
                 new Builder(mXmppConnectionService, "delivery_failed")
                         .setContentTitle(conversation.getName())
                         .setAutoCancel(true)
-                        .setSmallIcon(R.drawable.ic_error_white_24dp)
+                        .setSmallIcon(R.drawable.ic_error_24dp)
                         .setContentText(
                                 mXmppConnectionService
                                         .getResources()
@@ -502,7 +571,7 @@ public class NotificationService {
                                         .getQuantityText(
                                                 R.plurals.some_messages_could_not_be_delivered,
                                                 1024))
-                        .setSmallIcon(R.drawable.ic_error_white_24dp)
+                        .setSmallIcon(R.drawable.ic_error_24dp)
                         .setGroup("delivery_failed")
                         .setGroupSummary(true)
                         .setAutoCancel(true)
@@ -574,54 +643,13 @@ public class NotificationService {
             return;
         }
 
-        showIncomingCallNotification(id, media);
-        final NotificationManager notificationManager =
-                (NotificationManager)
-                        mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
-        final int currentInterruptionFilter;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) {
-            currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter();
-        } else {
-            currentInterruptionFilter = 1; // INTERRUPTION_FILTER_ALL
-        }
-        if (currentInterruptionFilter != 1) {
-            Log.d(
-                    Config.LOGTAG,
-                    "do not ring or vibrate because interruption filter has been set to "
-                            + currentInterruptionFilter);
-            return;
-        }
-        final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
-        this.vibrationFuture =
-                SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
-                        new VibrationRunnable(), 0, 3, TimeUnit.SECONDS);
-        if (currentVibrationFuture != null) {
-            currentVibrationFuture.cancel(true);
-        }
-        final SharedPreferences preferences =
-                PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
-        final Resources resources = mXmppConnectionService.getResources();
-        final String ringtonePreference =
-                preferences.getString(
-                        "call_ringtone", resources.getString(R.string.incoming_call_ringtone));
-        if (Strings.isNullOrEmpty(ringtonePreference)) {
-            Log.d(Config.LOGTAG, "ringtone has been set to none");
-            return;
-        }
-        final Uri uri = Uri.parse(ringtonePreference);
-        this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
-        if (this.currentlyPlayingRingtone == null) {
-            Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri);
-            return;
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-            this.currentlyPlayingRingtone.setLooping(true);
-        }
-        this.currentlyPlayingRingtone.play();
+        showIncomingCallNotification(id, media, false);
     }
 
     private void showIncomingCallNotification(
-            final AbstractJingleConnection.Id id, final Set<Media> media) {
+            final AbstractJingleConnection.Id id,
+            final Set<Media> media,
+            final boolean onlyAlertOnce) {
         final Intent fullScreenIntent =
                 new Intent(mXmppConnectionService, RtpSessionActivity.class);
         fullScreenIntent.putExtra(
@@ -631,9 +659,22 @@ public class NotificationService {
         fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
         fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        final int channelIteration;
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            channelIteration = getCurrentIncomingCallChannelIteration(mXmppConnectionService).or(0);
+        } else {
+            channelIteration = 0;
+        }
+        final var channelId = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + channelIteration;
+        Log.d(
+                Config.LOGTAG,
+                "showing incoming call notification on channel "
+                        + channelId
+                        + ", onlyAlertOnce="
+                        + onlyAlertOnce);
         final NotificationCompat.Builder builder =
                 new NotificationCompat.Builder(
-                        mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL);
+                        mXmppConnectionService, channelId);
         final Contact contact = id.getContact();
         builder.addPerson(getPerson(contact));
         ShortcutInfoCompat info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
@@ -654,12 +695,12 @@ public class NotificationService {
         );
         if (media.contains(Media.VIDEO)) {
             style.setIsVideo(true);
-            builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
+            builder.setSmallIcon(R.drawable.ic_videocam_24dp);
             builder.setContentTitle(
                     mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
         } else {
             style.setIsVideo(false);
-            builder.setSmallIcon(R.drawable.ic_call_white_24dp);
+            builder.setSmallIcon(R.drawable.ic_call_24dp);
             builder.setContentTitle(
                     mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
         }
@@ -672,16 +713,45 @@ public class NotificationService {
         if (systemAccount != null) {
             builder.addPerson(systemAccount.toString());
         }
+        if (!onlyAlertOnce) {
+            final var appSettings = new AppSettings(mXmppConnectionService);
+            final var ringtone = appSettings.getRingtone();
+            if (ringtone != null) {
+                builder.setSound(ringtone, AudioManager.STREAM_RING);
+            }
+            builder.setVibrate(CALL_PATTERN);
+        }
+        builder.setOnlyAlertOnce(onlyAlertOnce);
         builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
         builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
         builder.setPriority(NotificationCompat.PRIORITY_HIGH);
         builder.setCategory(NotificationCompat.CATEGORY_CALL);
-        PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
+        final PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
         builder.setFullScreenIntent(pendingIntent, true);
         builder.setContentIntent(pendingIntent); // old androids need this?
         builder.setOngoing(true);
-        modifyIncomingCall(builder, contact.getAccount());
+        builder.addAction(
+                new NotificationCompat.Action.Builder(
+                                R.drawable.ic_call_end_24dp,
+                                mXmppConnectionService.getString(R.string.dismiss_call),
+                                createCallAction(
+                                        id.sessionId,
+                                        XmppConnectionService.ACTION_DISMISS_CALL,
+                                        102))
+                        .build());
+        builder.addAction(
+                new NotificationCompat.Action.Builder(
+                                R.drawable.ic_call_24dp,
+                                mXmppConnectionService.getString(R.string.answer_call),
+                                createPendingRtpSession(
+                                        id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103))
+                        .build());
+        modifyIncomingCall(builder, id.account);
         final Notification notification = builder.build();
+        notification.audioAttributes = new AudioAttributes.Builder()
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .build();
         notification.flags = notification.flags | Notification.FLAG_INSISTENT;
         notify(INCOMING_CALL_NOTIFICATION_ID, notification);
     }
@@ -698,7 +768,7 @@ public class NotificationService {
         );
         if (ongoingCall.media.contains(Media.VIDEO)) {
             style.setIsVideo(true);
-            builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
+            builder.setSmallIcon(R.drawable.ic_videocam_24dp);
             if (ongoingCall.reconnecting) {
                 builder.setContentTitle(
                         mXmppConnectionService.getString(R.string.reconnecting_video_call));
@@ -708,7 +778,7 @@ public class NotificationService {
             }
         } else {
             style.setIsVideo(false);
-            builder.setSmallIcon(R.drawable.ic_call_white_24dp);
+            builder.setSmallIcon(R.drawable.ic_call_24dp);
             if (ongoingCall.reconnecting) {
                 builder.setContentTitle(
                         mXmppConnectionService.getString(R.string.reconnecting_call));
@@ -725,7 +795,7 @@ public class NotificationService {
         builder.setOngoing(true);
         builder.addAction(
                 new NotificationCompat.Action.Builder(
-                                R.drawable.ic_call_end_white_48dp,
+                                R.drawable.ic_call_end_24dp,
                                 mXmppConnectionService.getString(R.string.hang_up),
                                 createCallAction(
                                         id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
@@ -754,25 +824,25 @@ public class NotificationService {
     }
 
     public void cancelIncomingCallNotification() {
-        stopSoundAndVibration();
         cancel(INCOMING_CALL_NOTIFICATION_ID);
     }
 
     public boolean stopSoundAndVibration() {
-        int stopped = 0;
-        if (this.currentlyPlayingRingtone != null) {
-            if (this.currentlyPlayingRingtone.isPlaying()) {
-                Log.d(Config.LOGTAG, "stop playing ring tone");
-                ++stopped;
-            }
-            this.currentlyPlayingRingtone.stop();
+        final var jingleRtpConnection =
+                mXmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection();
+        if (jingleRtpConnection == null) {
+            return false;
         }
-        if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
-            Log.d(Config.LOGTAG, "stop vibration");
-            this.vibrationFuture.cancel(true);
-            ++stopped;
+        final var notificationManager = mXmppConnectionService.getSystemService(NotificationManager.class);
+        if (Iterables.any(
+                Arrays.asList(notificationManager.getActiveNotifications()),
+                n -> n.getId() == INCOMING_CALL_NOTIFICATION_ID)) {
+            Log.d(Config.LOGTAG, "stopping sound and vibration for incoming call notification");
+            showIncomingCallNotification(
+                    jingleRtpConnection.getId(), jingleRtpConnection.getMedia(), true);
+            return true;
         }
-        return stopped > 0;
+        return false;
     }
 
     public static void cancelIncomingCallNotification(final Context context) {
@@ -863,7 +933,8 @@ public class NotificationService {
 
     public void clearMissedCall(final Message message) {
         synchronized (mMissedCalls) {
-            final Iterator<Map.Entry<Conversational,MissedCallsInfo>> iterator = mMissedCalls.entrySet().iterator();
+            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();
@@ -871,7 +942,10 @@ public class NotificationService {
                 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");
+                        Log.d(
+                                Config.LOGTAG,
+                                conversational.getAccount().getJid().asBareJid()
+                                        + ": dismissed missed call because call was picked up on other device");
                         iterator.remove();
                     }
                 }
@@ -904,7 +978,7 @@ public class NotificationService {
     }
 
     private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
-        if (messages != null && messages.size() > 0) {
+        if (messages != null && !messages.isEmpty()) {
             Message last = messages.get(messages.size() - 1);
             if (last.getStatus() != Message.STATUS_RECEIVED) {
                 if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) {
@@ -920,7 +994,7 @@ public class NotificationService {
 		      color = account.getColor(false);
         } else {
             TypedValue typedValue = new TypedValue();
-            mXmppConnectionService.getTheme().resolveAttribute(R.attr.colorPrimary, typedValue, true);
+            mXmppConnectionService.getTheme().resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue, true);
             color = typedValue.data;
         }
         mBuilder.setColor(color);
@@ -952,7 +1026,7 @@ public class NotificationService {
                                 == 1; // if this check is changed to > 0 catchup messages will
         // create one notification per conversation
 
-        if (notifications.size() == 0) {
+        if (notifications.isEmpty()) {
             cancel(NOTIFICATION_ID);
         } else {
             if (notify) {
@@ -960,9 +1034,8 @@ public class NotificationService {
             }
             final Builder mBuilder;
             if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
-                ArrayList<Message> messages = notifications.values().iterator().next();
-                final Account account = messages.isEmpty() ? null : messages.get(0).getConversation().getAccount();
-                mBuilder = buildSingleConversations(messages, notify, isQuietHours(account));
+                final Account account = notifications.values().iterator().next().get(0).getConversation().getAccount();
+                mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify, isQuietHours(account));
                 modifyForSoundVibrationAndLight(mBuilder, notify, preferences, account);
                 notify(NOTIFICATION_ID, mBuilder.build());
             } else {
@@ -1023,16 +1096,18 @@ public class NotificationService {
         final Resources resources = mXmppConnectionService.getResources();
         final String ringtone =
                 preferences.getString(
-                        "notification_ringtone",
+                        AppSettings.NOTIFICATION_RINGTONE,
                         resources.getString(R.string.notification_ringtone));
         final boolean vibrate =
                 preferences.getBoolean(
-                        "vibrate_on_notification",
+                        AppSettings.NOTIFICATION_VIBRATE,
                         resources.getBoolean(R.bool.vibrate_on_notification));
-        final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led));
+        final boolean led =
+                preferences.getBoolean(
+                        AppSettings.NOTIFICATION_LED, resources.getBoolean(R.bool.led));
         final boolean headsup =
                 preferences.getBoolean(
-                        "notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
+                        AppSettings.NOTIFICATION_HEADS_UP, resources.getBoolean(R.bool.headsup_notifications));
         if (notify && !isQuietHours(account)) {
             if (vibrate) {
                 final int dat = 70;
@@ -1121,7 +1196,7 @@ public class NotificationService {
         if (!publicVersion) {
             builder.setContentText(Joiner.on(", ").join(names));
         }
-        builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
+        builder.setSmallIcon(R.drawable.ic_call_missed_24db);
         builder.setGroupSummary(true);
         builder.setGroup(MISSED_CALLS_GROUP);
         builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
@@ -1174,7 +1249,7 @@ public class NotificationService {
                                     name));
             builder.setContentText(name);
         }
-        builder.setSmallIcon(R.drawable.ic_call_missed_white_24db);
+        builder.setSmallIcon(R.drawable.ic_call_missed_24db);
         builder.setGroup(MISSED_CALLS_GROUP);
         builder.setCategory(NotificationCompat.CATEGORY_CALL);
         builder.setWhen(info.getLastTime());
@@ -1209,7 +1284,7 @@ public class NotificationService {
         final Builder mBuilder =
                 new NotificationCompat.Builder(
                         mXmppConnectionService,
-                        quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages"));
+                        notify && !quietHours ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages");
         final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
         style.setBigContentTitle(
                 mXmppConnectionService
@@ -1275,7 +1350,7 @@ public class NotificationService {
 
     private Builder buildSingleConversations(
             final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
-        final var channel = quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages");
+        final var channel = notify && !quietHours ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages";
         final Builder notificationBuilder =
                 new NotificationCompat.Builder(mXmppConnectionService, channel);
         if (messages.isEmpty()) {
@@ -1310,7 +1385,7 @@ public class NotificationService {
             PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
             NotificationCompat.Action markReadAction =
                     new NotificationCompat.Action.Builder(
-                                    R.drawable.ic_drafts_white_24dp,
+                                    R.drawable.ic_mark_chat_read_24dp,
                                     mXmppConnectionService.getString(R.string.mark_as_read),
                                     markAsReadPendingIntent)
                             .setSemanticAction(
@@ -1321,7 +1396,7 @@ public class NotificationService {
             final String lastMessageUuid = Iterables.getLast(messages).getUuid();
             final NotificationCompat.Action replyAction =
                     new NotificationCompat.Action.Builder(
-                                    R.drawable.ic_send_text_offline,
+                                    R.drawable.ic_send_24dp,
                                     replyLabel,
                                     createReplyIntent(conversation, lastMessageUuid, false))
                             .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
@@ -1330,7 +1405,7 @@ public class NotificationService {
                             .build();
             final NotificationCompat.Action wearReplyAction =
                     new NotificationCompat.Action.Builder(
-                                    R.drawable.ic_wear_reply,
+                                    R.drawable.ic_reply_24dp,
                                     replyLabel,
                                     createReplyIntent(conversation, lastMessageUuid, true))
                             .addRemoteInput(remoteInput)
@@ -1349,7 +1424,7 @@ public class NotificationService {
                 PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
                 NotificationCompat.Action snoozeAction =
                         new NotificationCompat.Action.Builder(
-                                        R.drawable.ic_notifications_paused_white_24dp,
+                                        R.drawable.ic_notifications_paused_24dp,
                                         label,
                                         pendingSnoozeIntent)
                                 .build();
@@ -1368,7 +1443,7 @@ public class NotificationService {
                                         .getString(R.string.show_location);
                         NotificationCompat.Action locationAction =
                                 new NotificationCompat.Action.Builder(
-                                                R.drawable.ic_room_white_24dp,
+                                                R.drawable.ic_location_pin_24dp,
                                                 label,
                                                 pendingShowLocationIntent)
                                         .build();
@@ -1392,7 +1467,7 @@ public class NotificationService {
                             createDownloadIntent(firstDownloadableMessage);
                     NotificationCompat.Action downloadAction =
                             new NotificationCompat.Action.Builder(
-                                            R.drawable.ic_file_download_white_24dp,
+                                            R.drawable.ic_download_24dp,
                                             label,
                                             pendingDownloadIntent)
                                     .build();
@@ -1795,21 +1870,34 @@ public class NotificationService {
     }
 
     private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
-        return pendingServiceIntent(mXmppConnectionService, action, requestCode, ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
+        return pendingServiceIntent(
+                mXmppConnectionService,
+                action,
+                requestCode,
+                ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
     }
 
     private PendingIntent createSnoozeIntent(final Conversation conversation) {
-        return pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_SNOOZE, generateRequestCode(conversation,22),ImmutableMap.of("uuid",conversation.getUuid()));
+        return pendingServiceIntent(
+                mXmppConnectionService,
+                XmppConnectionService.ACTION_SNOOZE,
+                generateRequestCode(conversation, 22),
+                ImmutableMap.of("uuid", conversation.getUuid()));
     }
 
-    private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode) {
+    private static PendingIntent pendingServiceIntent(
+            final Context context, final String action, final int requestCode) {
         return pendingServiceIntent(context, action, requestCode, ImmutableMap.of());
     }
 
-    private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode, final Map<String,String> extras) {
+    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()) {
+        for (final Map.Entry<String, String> entry : extras.entrySet()) {
             intent.putExtra(entry.getKey(), entry.getValue());
         }
         return PendingIntent.getService(
@@ -1830,7 +1918,6 @@ public class NotificationService {
             if (sender != null && sender.getAffiliation().ranks(MucOptions.Affiliation.MEMBER) && message.isAttention()) {
                 return true;
             }
-
             final String nick = conversation.getMucOptions().getActualNick();
             final Pattern highlight = generateNickHighlightPattern(nick);
             final String name = conversation.getMucOptions().getActualName();
@@ -1890,8 +1977,7 @@ public class NotificationService {
             connected = 0;
         } else {
             enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled));
-            connected =
-                    Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
+            connected = Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
         }
         mBuilder.setContentText(
                 mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
@@ -1901,23 +1987,20 @@ public class NotificationService {
         }
         mBuilder.setWhen(0)
                 .setPriority(Notification.PRIORITY_MIN)
-                .setSmallIcon(
-                        connected > 0
-                                ? R.drawable.ic_link_white_24dp
-                                : R.drawable.ic_link_off_white_24dp)
+                .setSmallIcon(connected > 0 ? R.drawable.ic_link_24dp : R.drawable.ic_link_off_24dp)
                 .setLocalOnly(true);
 
         if (Compatibility.runsTwentySix()) {
             mBuilder.setChannelId("foreground");
             mBuilder.addAction(
-                    R.drawable.ic_logout_white_24dp,
+                    R.drawable.ic_logout_24dp,
                     mXmppConnectionService.getString(R.string.log_out),
                     pendingServiceIntent(
                             mXmppConnectionService,
                             XmppConnectionService.ACTION_TEMPORARILY_DISABLE,
                             87));
             mBuilder.addAction(
-                    R.drawable.ic_notifications_off_white_24dp,
+                    R.drawable.ic_notifications_off_24dp,
                     mXmppConnectionService.getString(R.string.hide_notification),
                     pendingNotificationSettingsIntent(mXmppConnectionService));
         }
@@ -1971,10 +2054,17 @@ public class NotificationService {
             }
         }
         if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) {
-            notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
+            try {
+                notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
+            } catch (final RuntimeException e) {
+                Log.d(
+                        Config.LOGTAG,
+                        "not refreshing foreground service notification because service has died",
+                        e);
+            }
         }
         final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
-        if (errors.size() == 0) {
+        if (errors.isEmpty()) {
             cancel(ERROR_NOTIFICATION_ID);
             return;
         } else if (errors.size() == 1) {
@@ -1986,14 +2076,27 @@ public class NotificationService {
                     mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
             mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
         }
-        mBuilder.addAction(
-                R.drawable.ic_autorenew_white_24dp,
-                mXmppConnectionService.getString(R.string.try_again),
-                pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
+        try {
+            mBuilder.addAction(
+                    R.drawable.ic_autorenew_24dp,
+                    mXmppConnectionService.getString(R.string.try_again),
+                    pendingServiceIntent(
+                            mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
+            mBuilder.setDeleteIntent(
+                    pendingServiceIntent(
+                            mXmppConnectionService,
+                            XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS,
+                            69));
+        } catch (final RuntimeException e) {
+            Log.d(
+                    Config.LOGTAG,
+                    "not including some actions in error notification because service has died",
+                    e);
+        }
         if (torNotAvailable) {
             if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
                 mBuilder.addAction(
-                        R.drawable.ic_play_circle_filled_white_48dp,
+                        R.drawable.ic_play_circle_24dp,
                         mXmppConnectionService.getString(R.string.start_orbot),
                         PendingIntent.getActivity(
                                 mXmppConnectionService,
@@ -2005,7 +2108,7 @@ public class NotificationService {
                                         : PendingIntent.FLAG_UPDATE_CURRENT));
             } else {
                 mBuilder.addAction(
-                        R.drawable.ic_file_download_white_24dp,
+                        R.drawable.ic_download_24dp,
                         mXmppConnectionService.getString(R.string.install_orbot),
                         PendingIntent.getActivity(
                                 mXmppConnectionService,
@@ -2017,9 +2120,8 @@ public class NotificationService {
                                         : PendingIntent.FLAG_UPDATE_CURRENT));
             }
         }
-        mBuilder.setDeleteIntent(pendingServiceIntent(mXmppConnectionService,XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, 69));
         mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
-        mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
+        mBuilder.setSmallIcon(R.drawable.ic_warning_24dp);
         mBuilder.setLocalOnly(true);
         mBuilder.setPriority(Notification.PRIORITY_LOW);
         final Intent intent;
@@ -2044,41 +2146,66 @@ public class NotificationService {
         notify(ERROR_NOTIFICATION_ID, mBuilder.build());
     }
 
-    void updateFileAddingNotification(int current, Message message) {
-        Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
-        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
-        mBuilder.setProgress(100, current, false);
-        mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp);
-        mBuilder.setContentIntent(createContentIntent(message.getConversation()));
-        mBuilder.setOngoing(true);
+    void updateFileAddingNotification(final int current, final Message message) {
+
+        final Notification notification = videoTranscoding(current, message);
+        notify(ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID, notification);
+    }
+
+    private Notification videoTranscoding(final int current, @Nullable final Message message) {
+        final Notification.Builder builder = new Notification.Builder(mXmppConnectionService);
+        builder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video));
+        if (current >= 0) {
+            builder.setProgress(100, current, false);
+        } else {
+            builder.setProgress(100, 0, true);
+        }
+        builder.setSmallIcon(R.drawable.ic_hourglass_top_24dp);
+        if (message != null) {
+            builder.setContentIntent(createContentIntent(message.getConversation()));
+        }
+        builder.setOngoing(true);
         if (Compatibility.runsTwentySix()) {
-            mBuilder.setChannelId("compression");
+            builder.setChannelId("compression");
         }
-        Notification notification = mBuilder.build();
-        notify(FOREGROUND_NOTIFICATION_ID, notification);
+        return builder.build();
     }
 
-    private void notify(String tag, int id, Notification notification) {
-        final NotificationManagerCompat notificationManager =
-                NotificationManagerCompat.from(mXmppConnectionService);
+    public Notification getIndeterminateVideoTranscoding() {
+        return videoTranscoding(-1, null);
+    }
+
+    private void notify(final String tag, final int id, final Notification notification) {
+        if (ActivityCompat.checkSelfPermission(
+                        mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS)
+                != PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+        final var notificationManager =
+                mXmppConnectionService.getSystemService(NotificationManager.class);
         try {
             notificationManager.notify(tag, id, notification);
-        } catch (RuntimeException e) {
+        } catch (final RuntimeException e) {
             Log.d(Config.LOGTAG, "unable to make notification", e);
         }
     }
 
-    public void notify(int id, Notification notification) {
-        final NotificationManagerCompat notificationManager =
-                NotificationManagerCompat.from(mXmppConnectionService);
+    public void notify(final int id, final Notification notification) {
+        if (ActivityCompat.checkSelfPermission(
+                        mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS)
+                != PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+        final var notificationManager =
+                mXmppConnectionService.getSystemService(NotificationManager.class);
         try {
             notificationManager.notify(id, notification);
-        } catch (RuntimeException e) {
+        } catch (final RuntimeException e) {
             Log.d(Config.LOGTAG, "unable to make notification", e);
         }
     }
 
-    public void cancel(int id) {
+    public void cancel(final int id) {
         final NotificationManagerCompat notificationManager =
                 NotificationManagerCompat.from(mXmppConnectionService);
         try {
@@ -2125,14 +2252,4 @@ public class NotificationService {
             return lastTime;
         }
     }
-
-    private class VibrationRunnable implements Runnable {
-
-        @Override
-        public void run() {
-            final Vibrator vibrator =
-                    (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
-            vibrator.vibrate(CALL_PATTERN, -1);
-			}
-		}
 }

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

@@ -44,8 +44,6 @@ import android.preference.PreferenceManager;
 import android.provider.ContactsContract;
 import android.provider.DocumentsContract;
 import android.security.KeyChain;
-import android.telephony.PhoneStateListener;
-import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -55,6 +53,7 @@ import android.util.Pair;
 import androidx.annotation.BoolRes;
 import androidx.annotation.IntegerRes;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.core.app.RemoteInput;
 import androidx.core.content.ContextCompat;
 import androidx.core.util.Consumer;
@@ -66,6 +65,8 @@ import com.google.common.base.Objects;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Multimap;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
 import com.google.common.io.Files;
 
 import com.kedia.ogparser.JsoupProxy;
@@ -103,6 +104,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.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -111,6 +113,7 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import io.ipfs.cid.Cid;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.android.JabberIdContact;
@@ -149,7 +152,6 @@ 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;
 import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
@@ -177,6 +179,7 @@ import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.LocalizedContent;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.InvalidJid;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnBindListener;
 import eu.siacs.conversations.xmpp.OnContactStatusChanged;
@@ -227,6 +230,7 @@ public class XmppConnectionService extends Service {
     public static final String ACTION_END_CALL = "end_call";
     public static final String ACTION_STARTING_CALL = "starting_call";
     public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
+    public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED = "call_integration_service_started";
     private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
     public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
     public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
@@ -272,7 +276,7 @@ public class XmppConnectionService extends Service {
     private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this);
     private final ShortcutService mShortcutService = new ShortcutService(this);
     private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
-    private final AtomicBoolean mForceForegroundService = new AtomicBoolean(false);
+    private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false);
     private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false);
     private final AtomicReference<OngoingCall> ongoingCall = new AtomicReference<>();
     private final OnMessagePacketReceived mMessageParser = new MessageParser(this);
@@ -336,18 +340,8 @@ public class XmppConnectionService extends Service {
             return false;
         }
     };
-    private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false);
+
     private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
-    private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
-        @Override
-        public void onCallStateChanged(final int state, final String phoneNumber) {
-            if (diallerIntegrationActive.get()) return;
-            isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE);
-            if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
-                mJingleConnectionManager.notifyPhoneCallStarted();
-            }
-        }
-    };
 
     public void setDiallerIntegrationActive(boolean active) {
       diallerIntegrationActive.set(active);
@@ -417,16 +411,27 @@ public class XmppConnectionService extends Service {
             } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) {
                 fetchBookmarks(account);
             }
+
+            if (connection.getFeatures().mds()) {
+                fetchMessageDisplayedSynchronization(account);
+            } else {
+                Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds");
+            }
             final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval();
             final boolean catchup = getMessageArchiveService().inCatchup(account);
+            final boolean trackOfflineMessageRetrieval;
             if (flexible && catchup && account.getXmppConnection().isMamPreferenceAlways()) {
+                trackOfflineMessageRetrieval = false;
                 sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> {
                     if (packet.getType() == IqPacket.TYPE.RESULT) {
                         Log.d(Config.LOGTAG, acc.getJid().asBareJid() + ": successfully purged offline messages");
                     }
                 });
+            } else {
+                trackOfflineMessageRetrieval = true;
             }
             sendPresence(account);
+            account.getXmppConnection().trackOfflineMessageRetrieval(trackOfflineMessageRetrieval);
             if (mPushManagementService.available(account)) {
                 mPushManagementService.registerPushTokenOnServer(account);
             }
@@ -436,6 +441,7 @@ public class XmppConnectionService extends Service {
             unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account);
         }
     };
+
     private final AtomicLong mLastExpiryRun = new AtomicLong(0);
     private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20);
     private final OnStatusChanged statusListener = new OnStatusChanged() {
@@ -560,13 +566,13 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void startForcingForegroundNotification() {
-        mForceForegroundService.set(true);
+    public void startOngoingVideoTranscodingForegroundNotification() {
+        mOngoingVideoTranscoding.set(true);
         toggleForegroundService();
     }
 
-    public void stopForcingForegroundNotification() {
-        mForceForegroundService.set(false);
+    public void stopOngoingVideoTranscodingForegroundNotification() {
+        mOngoingVideoTranscoding.set(false);
         toggleForegroundService();
     }
 
@@ -1036,9 +1042,7 @@ public class XmppConnectionService extends Service {
                 }
                 break;
             case ACTION_IDLE_PING:
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                    scheduleNextIdlePing();
-                }
+                scheduleNextIdlePing();
                 break;
             case ACTION_FCM_MESSAGE_RECEIVED:
                 Log.d(Config.LOGTAG, "push message arrived in service. account");
@@ -1062,7 +1066,12 @@ public class XmppConnectionService extends Service {
                 }
                 return START_NOT_STICKY;
         }
-        new Thread(() -> manageAccountConnectionStates(action, intent == null ? null : intent.getExtras())).start();
+        final var extras =  intent == null ? null : intent.getExtras();
+        try {
+            internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras));
+        } catch (final RejectedExecutionException e) {
+            Log.e(Config.LOGTAG, "can not schedule connection states manager");
+        }
         if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
             expireOldMessages();
         }
@@ -1152,7 +1161,7 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
+    private boolean processAccountState(final Account account, final boolean interactive, final boolean isUiAction, final boolean isAccountPushed, final HashSet<Account> pingCandidates) {
         if (!account.getStatus().isAttemptReconnect()) {
             return false;
         }
@@ -1247,15 +1256,13 @@ public class XmppConnectionService extends Service {
     }
 
     public boolean isDataSaverDisabled() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            final ConnectivityManager connectivityManager =
-                    (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
-            return !connectivityManager.isActiveNetworkMetered()
-                    || Compatibility.getRestrictBackgroundStatus(connectivityManager)
-                            == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-        } else {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
             return true;
         }
+        final ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class);
+        return !Compatibility.isActiveNetworkMetered(connectivityManager)
+                || Compatibility.getRestrictBackgroundStatus(connectivityManager)
+                        == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
     }
 
     private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
@@ -1307,19 +1314,19 @@ public class XmppConnectionService extends Service {
     }
 
     private boolean dndOnSilentMode() {
-        return getBooleanPreference(SettingsActivity.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
+        return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
     }
 
     private boolean manuallyChangePresence() {
-        return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
+        return getBooleanPreference(AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
     }
 
     private boolean treatVibrateAsSilent() {
-        return getBooleanPreference(SettingsActivity.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
+        return getBooleanPreference(AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
     }
 
     private boolean awayWhenScreenLocked() {
-        return getBooleanPreference(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
+        return getBooleanPreference(AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
     }
 
     private String getCompressPicturesPreference() {
@@ -1337,23 +1344,23 @@ public class XmppConnectionService extends Service {
     }
 
     public boolean isScreenLocked() {
-        final KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
-        final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+        final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
+        final PowerManager powerManager = getSystemService(PowerManager.class);
         final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
-        final boolean interactive = powerManager != null && powerManager.isInteractive();
+        final boolean interactive;
+        try {
+            interactive = powerManager != null && powerManager.isInteractive();
+        } catch (final Exception e) {
+            return false;
+        }
         return locked || !interactive;
     }
 
     private boolean isPhoneSilenced() {
-        final boolean notificationDnd;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            final NotificationManager notificationManager = getSystemService(NotificationManager.class);
-            final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter();
-            notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-        } else {
-            notificationDnd = false;
-        }
-        final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+        final NotificationManager notificationManager = getSystemService(NotificationManager.class);
+        final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter();
+        final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+        final AudioManager audioManager = getSystemService(AudioManager.class);
         final int ringerMode = audioManager == null ? AudioManager.RINGER_MODE_NORMAL : audioManager.getRingerMode();
         try {
             if (treatVibrateAsSilent()) {
@@ -1361,7 +1368,7 @@ public class XmppConnectionService extends Service {
             } else {
                 return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
             }
-        } catch (Throwable throwable) {
+        } catch (final Throwable throwable) {
             Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
             return notificationDnd;
         }
@@ -1443,7 +1450,7 @@ public class XmppConnectionService extends Service {
     public void onCreate() {
         LibIdnXmppStringprep.setup();
         emojiSearch = new EmojiSearch(this);
-        setTheme(ThemeHelper.find(this));
+        setTheme(R.style.Theme_Conversations3);
         ThemeHelper.applyCustomColors(this);
         if (Compatibility.runsTwentySix()) {
             mNotificationService.initializeChannels();
@@ -1460,7 +1467,7 @@ public class XmppConnectionService extends Service {
             Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
         }
         Resolver.init(this);
-        updateMemorizingTrustmanager();
+        updateMemorizingTrustManager();
         final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
         final int cacheSize = maxMemory / 10;
         this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
@@ -1489,23 +1496,21 @@ public class XmppConnectionService extends Service {
             if (color != 0) account.setColor(color);
         }
         final SharedPreferences.Editor editor = getPreferences().edit();
-        if (this.accounts.size() == 0 && Arrays.asList("Sony", "Sony Ericsson").contains(Build.MANUFACTURER)) {
-            editor.putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, true);
-            Log.d(Config.LOGTAG, Build.MANUFACTURER + " is on blacklist. enabling foreground service");
-        }
         final boolean hasEnabledAccounts = hasEnabledAccounts();
         editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
         editor.apply();
         toggleSetProfilePictureActivity(hasEnabledAccounts);
         reconfigurePushDistributor();
 
+        if (CallIntegration.hasSystemFeature(this)) {
+            CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts);
+        }
+
         restoreFromDatabase();
 
         if (QuickConversationsService.isContactListIntegration(this)
-                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
-                        || ContextCompat.checkSelfPermission(
-                                        this, Manifest.permission.READ_CONTACTS)
-                                == PackageManager.PERMISSION_GRANTED)) {
+                && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
+                        == PackageManager.PERMISSION_GRANTED) {
             startContactObserver();
         }
         FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
@@ -1534,20 +1539,18 @@ public class XmppConnectionService extends Service {
             this.pgpServiceConnection.bindToService();
         }
 
-        final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class);
-        this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
+        final PowerManager powerManager = getSystemService(PowerManager.class);
+        this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
 
         toggleForegroundService();
         updateUnreadCountBadge();
         toggleScreenEventReceiver();
         final IntentFilter systemBroadcastFilter = new IntentFilter();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            scheduleNextIdlePing();
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
-            }
-            systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
+        scheduleNextIdlePing();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
         }
+        systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
         ContextCompat.registerReceiver(
                 this,
                 this.mInternalEventReceiver,
@@ -1562,24 +1565,22 @@ public class XmppConnectionService extends Service {
                 ContextCompat.RECEIVER_EXPORTED);
         mForceDuringOnCreate.set(false);
         toggleForegroundService();
-        setupPhoneStateListener();
         rescanStickers();
         internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS);
+        final SharedPreferences sharedPreferences =
+                androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
+        sharedPreferences.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
+            @Override
+            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
+                Log.d(Config.LOGTAG,"preference '"+key+"' has changed");
+                if (AppSettings.KEEP_FOREGROUND_SERVICE.equals(key)) {
+                    toggleForegroundService();
+                }
+            }
+        });
     }
 
 
-    private void setupPhoneStateListener() {
-        final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
-        if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            return;
-        }
-        telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
-    }
-
-    public boolean isPhoneInCall() {
-        return isPhoneInCall.get();
-    }
-
     private void checkForDeletedFiles() {
         if (destroyed) {
             Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed");
@@ -1681,36 +1682,47 @@ public class XmppConnectionService extends Service {
     private void toggleForegroundService(boolean force, boolean needMic) {
         final boolean status;
         final OngoingCall ongoing = ongoingCall.get();
-        final boolean showOngoing = ongoing != null && !diallerIntegrationActive.get();
-        if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || showOngoing || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
+        final boolean ongoingVideoTranscoding = mOngoingVideoTranscoding.get();
+        final int id;
+        if (force
+                || mForceDuringOnCreate.get()
+                || ongoingVideoTranscoding
+                || ongoing != null
+                || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
             final Notification notification;
-            final int id;
-            if (showOngoing) {
+            if (ongoing != null && !diallerIntegrationActive.get()) {
                 notification = this.mNotificationService.getOngoingCallNotification(ongoing);
                 id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
                 startForegroundOrCatch(id, notification, true);
-                mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
+            } else if (ongoingVideoTranscoding) {
+                notification = this.mNotificationService.getIndeterminateVideoTranscoding();
+                id = NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID;
+                startForegroundOrCatch(id, notification, false);
             } else {
                 notification = this.mNotificationService.createForegroundNotification();
                 id = NotificationService.FOREGROUND_NOTIFICATION_ID;
                 startForegroundOrCatch(id, notification, needMic || ongoing != null || diallerIntegrationActive.get());
             }
-
-            if (!mForceForegroundService.get()) {
-                mNotificationService.notify(id, notification);
-            }
+            mNotificationService.notify(id, notification);
             status = true;
         } else {
+            id = 0;
             stopForeground(true);
             status = false;
         }
-        if (!mForceForegroundService.get()) {
-            mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
-        }
-        if (!showOngoing) {
-            mNotificationService.cancel(NotificationService.ONGOING_CALL_NOTIFICATION_ID);
+
+        for (final int toBeRemoved :
+                Collections2.filter(
+                        Arrays.asList(
+                                NotificationService.FOREGROUND_NOTIFICATION_ID,
+                                NotificationService.ONGOING_CALL_NOTIFICATION_ID,
+                                NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID),
+                        i -> i != id)) {
+            mNotificationService.cancel(toBeRemoved);
         }
-        Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off"));
+        Log.d(
+                Config.LOGTAG,
+                "ForegroundService: " + (status ? "on" : "off") + ", notification: " + id);
     }
 
     private void startForegroundOrCatch(
@@ -1747,13 +1759,13 @@ public class XmppConnectionService extends Service {
     }
 
     public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
-        return !mForceForegroundService.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts();
+        return !mOngoingVideoTranscoding.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts();
     }
 
     @Override
     public void onTaskRemoved(final Intent rootIntent) {
         super.onTaskRemoved(rootIntent);
-        if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get() || ongoingCall.get() != null) {
+        if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mOngoingVideoTranscoding.get() || ongoingCall.get() != null) {
             Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
         } else {
             this.logoutAndSave(false);
@@ -2263,26 +2275,95 @@ public class XmppConnectionService extends Service {
 
     public void fetchBookmarks2(final Account account) {
         final IqPacket retrieve = mIqGenerator.retrieveBookmarks();
-        sendIqPacket(account, retrieve, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(final Account account, final IqPacket response) {
-                if (response.getType() == IqPacket.TYPE.RESULT) {
-                    final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
-                    final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromPubsub(pubsub, account);
-                    processBookmarksInitial(account, bookmarks, true);
-                }
+        sendIqPacket(account, retrieve, (a, response) -> {
+            if (response.getType() == IqPacket.TYPE.RESULT) {
+                final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
+                final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromPubsub(pubsub, a);
+                processBookmarksInitial(a, bookmarks, true);
             }
         });
     }
 
-    public void processBookmarksInitial(Account account, Map<Jid, Bookmark> bookmarks, final boolean pep) {
+    private void fetchMessageDisplayedSynchronization(final Account account) {
+        Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
+        final var retrieve = mIqGenerator.retrieveMds();
+        sendIqPacket(
+                account,
+                retrieve,
+                (a, response) -> {
+                    if (response.getType() != IqPacket.TYPE.RESULT) {
+                        return;
+                    }
+                    final var pubSub = response.findChild("pubsub", Namespace.PUBSUB);
+                    final Element items = pubSub == null ? null : pubSub.findChild("items");
+                    if (items == null
+                            || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) {
+                        return;
+                    }
+                    for (final Element child : items.getChildren()) {
+                        if ("item".equals(child.getName())) {
+                            processMdsItem(account, child);
+                        }
+                    }
+                });
+    }
+
+    public void processMdsItem(final Account account, final Element item) {
+        final Jid jid =
+                item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("id"));
+        if (jid == null) {
+            return;
+        }
+        final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED);
+        final Element stanzaId =
+                displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS);
+        final String id = stanzaId == null ? null : stanzaId.getAttribute("id");
+        final Conversation conversation = find(account, jid);
+        if (id != null && conversation != null) {
+            conversation.setDisplayState(id);
+            markReadUpToStanzaId(conversation, id);
+        }
+    }
+
+    public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
+        final Message message = conversation.findMessageWithServerMsgId(stanzaId);
+        if (message == null) { // do we want to check if isRead?
+            return;
+        }
+        markReadUpTo(conversation, message);
+    }
+
+    public void markReadUpTo(final Conversation conversation, final Message message) {
+        final boolean isDismissNotification = isDismissNotification(message);
+        final var uuid = message.getUuid();
+        Log.d(
+                Config.LOGTAG,
+                conversation.getAccount().getJid().asBareJid()
+                        + ": mark "
+                        + conversation.getJid().asBareJid()
+                        + " as read up to "
+                        + uuid);
+        markRead(conversation, uuid, isDismissNotification);
+    }
+
+    private static boolean isDismissNotification(final Message message) {
+        Message next = message.next();
+        while (next != null) {
+            if (message.getStatus() == Message.STATUS_RECEIVED) {
+                return false;
+            }
+            next = next.next();
+        }
+        return true;
+    }
+
+    public void processBookmarksInitial(final Account account, final Map<Jid, Bookmark> bookmarks, final boolean pep) {
         final Set<Jid> previousBookmarks = account.getBookmarkedJids();
-        final boolean synchronizeWithBookmarks = synchronizeWithBookmarks();
-        for (Bookmark bookmark : bookmarks.values()) {
+        for (final Bookmark bookmark : bookmarks.values()) {
             previousBookmarks.remove(bookmark.getJid().asBareJid());
-            processModifiedBookmark(bookmark, pep, synchronizeWithBookmarks);
+            processModifiedBookmark(bookmark, pep);
         }
-        if (pep && synchronizeWithBookmarks) {
+        if (pep) {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + previousBookmarks.size() + " bookmarks have been removed");
             for (Jid jid : previousBookmarks) {
                 processDeletedBookmark(account, jid);
@@ -2299,7 +2380,7 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    private void processModifiedBookmark(Bookmark bookmark, final boolean pep, final boolean synchronizeWithBookmarks) {
+    private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
         final Account account = bookmark.getAccount();
         Conversation conversation = find(bookmark);
         if (conversation != null) {
@@ -2307,7 +2388,7 @@ public class XmppConnectionService extends Service {
                 return;
             }
             bookmark.setConversation(conversation);
-            if (pep && synchronizeWithBookmarks && !bookmark.autojoin()) {
+            if (pep && !bookmark.autojoin()) {
                 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conference (" + conversation.getJid() + ") after receiving pep");
                 archiveConversation(conversation, false);
             } else {
@@ -2321,15 +2402,14 @@ public class XmppConnectionService extends Service {
                     }
                 }
             }
-        } else if (synchronizeWithBookmarks && bookmark.autojoin()) {
+        } else if (bookmark.autojoin()) {
             conversation = findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
             bookmark.setConversation(conversation);
         }
     }
 
-    public void processModifiedBookmark(Bookmark bookmark) {
-        final boolean synchronizeWithBookmarks = synchronizeWithBookmarks();
-        processModifiedBookmark(bookmark, true, synchronizeWithBookmarks);
+    public void processModifiedBookmark(final Bookmark bookmark) {
+        processModifiedBookmark(bookmark, true);
     }
 
     public void createBookmark(final Account account, final Bookmark bookmark) {
@@ -2420,7 +2500,7 @@ public class XmppConnectionService extends Service {
                     }
                 });
             } else {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing bookmarks (retry=" + retry + ") " + response);
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing "+node+" (retry=" + retry + ") " + response);
             }
         });
     }
@@ -2883,7 +2963,7 @@ public class XmppConnectionService extends Service {
             if (conversation.getMode() == Conversation.MODE_MULTI) {
                 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
                     final Bookmark bookmark = conversation.getBookmark();
-                    if (maySynchronizeWithBookmarks && bookmark != null && synchronizeWithBookmarks()) {
+                    if (maySynchronizeWithBookmarks && bookmark != null) {
                         if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
                             Account account = bookmark.getAccount();
                             bookmark.setConversation(null);
@@ -2916,6 +2996,9 @@ public class XmppConnectionService extends Service {
     public void createAccount(final Account account) {
         account.initAccountServices(this);
         databaseBackend.createAccount(account);
+        if (CallIntegration.hasSystemFeature(this)) {
+            CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
+        }
         this.accounts.add(account);
         this.reconnectAccountInBackground(account);
         updateAccountUi();
@@ -3045,6 +3128,9 @@ public class XmppConnectionService extends Service {
             toggleForegroundService();
             syncEnabledAccountSetting();
             mChannelDiscoveryService.cleanCache();
+            if (CallIntegration.hasSystemFeature(this)) {
+                CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
+            }
             return true;
         } else {
             return false;
@@ -3112,6 +3198,7 @@ public class XmppConnectionService extends Service {
             };
             mDatabaseWriterExecutor.execute(runnable);
             this.accounts.remove(account);
+            CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
             this.mRosterSyncTaskManager.clear(account);
             updateAccountUi();
             mNotificationService.updateErrorNotification();
@@ -3650,14 +3737,12 @@ public class XmppConnectionService extends Service {
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
     }
 
-    public void providePasswordForMuc(Conversation conversation, String password) {
+    public void providePasswordForMuc(final Conversation conversation, final String password) {
         if (conversation.getMode() == Conversation.MODE_MULTI) {
             conversation.getMucOptions().setPassword(password);
             if (conversation.getBookmark() != null) {
                 final Bookmark bookmark = conversation.getBookmark();
-                if (synchronizeWithBookmarks()) {
-                    bookmark.setAutojoin(true);
-                }
+                bookmark.setAutojoin(true);
                 createBookmark(conversation.getAccount(), bookmark);
             }
             updateConversation(conversation);
@@ -3756,7 +3841,7 @@ public class XmppConnectionService extends Service {
         new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start();
     }
 
-    public void persistSelfNick(MucOptions.User self) {
+    public void persistSelfNick(final MucOptions.User self) {
         final Conversation conversation = self.getConversation();
         final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark();
         Jid full = self.getFullJid();
@@ -3769,11 +3854,10 @@ public class XmppConnectionService extends Service {
         final String nick = self.getNick();
         final Bookmark bookmark = conversation.getBookmark();
         final String bookmarkedNick = bookmark == null ? null : bookmark.getNick();
-        if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !nick.equals(bookmarkedNick)) {
+        if (bookmark != null && (tookProposedNickFromBookmark || Strings.isNullOrEmpty(bookmarkedNick)) && !nick.equals(bookmarkedNick)) {
             final Account account = conversation.getAccount();
             final String defaultNick = MucOptions.defaultNick(account);
-            if (TextUtils.isEmpty(bookmarkedNick) && nick.equals(defaultNick)) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not overwrite empty bookmark nick with default nick for " + conversation.getJid().asBareJid());
+            if (Strings.isNullOrEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) {
                 return;
             }
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persist nick '" + nick + "' into bookmark for " + conversation.getJid().asBareJid());
@@ -4874,7 +4958,7 @@ public class XmppConnectionService extends Service {
     }
 
     public long getAutomaticMessageDeletionDate() {
-        final long timeout = getLongPreference(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion);
+        final long timeout = getLongPreference(AppSettings.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion);
         return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
     }
 
@@ -4903,10 +4987,6 @@ public class XmppConnectionService extends Service {
         return getBooleanPreference("chat_states", R.bool.chat_states);
     }
 
-    private boolean synchronizeWithBookmarks() {
-        return getBooleanPreference("autojoin", R.bool.autojoin);
-    }
-
     public boolean useTorToConnect() {
         return getBooleanPreference("use_tor", R.bool.use_tor);
     }
@@ -4916,7 +4996,7 @@ public class XmppConnectionService extends Service {
     }
 
     public boolean broadcastLastActivity() {
-        return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
+        return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
     }
 
     public int unreadCount() {
@@ -4930,7 +5010,7 @@ public class XmppConnectionService extends Service {
 
     private <T> List<T> threadSafeList(Set<T> set) {
         synchronized (LISTENER_LOCK) {
-            return set.size() == 0 ? Collections.emptyList() : new ArrayList<>(set);
+            return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
         }
     }
 
@@ -4956,7 +5036,7 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
+    public void notifyJingleRtpConnectionUpdate(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
         for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
             listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
         }
@@ -5083,24 +5163,101 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void sendReadMarker(final Conversation conversation, String upToUuid) {
-        final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous();
+    public void sendReadMarker(final Conversation conversation, final String upToUuid) {
+        final boolean isPrivateAndNonAnonymousMuc =
+                conversation.getMode() == Conversation.MODE_MULTI
+                        && conversation.isPrivateAndNonAnonymous();
         final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
-        if (readMessages.size() > 0) {
-            updateConversationUi();
+        if (readMessages.isEmpty()) {
+            return;
         }
-        final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc);
-        if (confirmMessages()
-                && markable != null
-                && (markable.trusted() || isPrivateAndNonAnonymousMuc)
-                && markable.getRemoteMsgId() != null) {
-            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
-            final Account account = conversation.getAccount();
-            final MessagePacket packet = mMessageGenerator.confirm(markable);
+        final var account = conversation.getAccount();
+        final var connection = account.getXmppConnection();
+        updateConversationUi();
+        final var last =
+                Iterables.getLast(
+                        Collections2.filter(
+                                readMessages,
+                                m ->
+                                        !m.isPrivateMessage()
+                                                && m.getStatus() == Message.STATUS_RECEIVED),
+                        null);
+        if (last == null) {
+            return;
+        }
+
+        final boolean sendDisplayedMarker =
+                confirmMessages()
+                        && (last.trusted() || isPrivateAndNonAnonymousMuc)
+                        && last.getRemoteMsgId() != null
+                        && (last.markable || isPrivateAndNonAnonymousMuc);
+        final boolean serverAssist =
+                connection != null && connection.getFeatures().mdsServerAssist();
+
+        final String stanzaId = last.getServerMsgId();
+
+        if (sendDisplayedMarker && serverAssist) {
+            final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
+            final MessagePacket packet = mMessageGenerator.confirm(last);
+            packet.addChild(mdsDisplayed);
+            if (!last.isPrivateMessage()) {
+                packet.setTo(packet.getTo().asBareJid());
+            }
+            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server assisted "+packet);
             this.sendMessagePacket(account, packet);
+        } else {
+            publishMds(last);
+            // read markers will be sent after MDS to flush the CSI stanza queue
+            if (sendDisplayedMarker) {
+                Log.d(
+                        Config.LOGTAG,
+                        conversation.getAccount().getJid().asBareJid()
+                                + ": sending displayed marker to "
+                                + last.getCounterpart().toString());
+                final MessagePacket packet = mMessageGenerator.confirm(last);
+                this.sendMessagePacket(account, packet);
+            }
         }
     }
 
+    private void publishMds(@Nullable final Message message) {
+        final String stanzaId = message == null ? null : message.getServerMsgId();
+        if (Strings.isNullOrEmpty(stanzaId)) {
+            return;
+        }
+        final Conversation conversation;
+        final var conversational = message.getConversation();
+        if (conversational instanceof Conversation c) {
+            conversation = c;
+        } else {
+            return;
+        }
+        final var account = conversation.getAccount();
+        final var connection = account.getXmppConnection();
+        if (connection == null || !connection.getFeatures().mds()) {
+            return;
+        }
+        final Jid itemId;
+        if (message.isPrivateMessage()) {
+            itemId = message.getCounterpart();
+        } else {
+            itemId = conversation.getJid().asBareJid();
+        }
+        Log.d(Config.LOGTAG,"publishing mds for "+itemId+"/"+stanzaId);
+        publishMds(account, itemId, stanzaId, conversation);
+    }
+
+    private void publishMds(
+            final Account account, final Jid itemId, final String stanzaId, final Conversation conversation) {
+        final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
+        pushNodeAndEnforcePublishOptions(
+                account,
+                Namespace.MDS_DISPLAYED,
+                item,
+                itemId.toEscapedString(),
+                PublishOptions.persistentWhitelistAccessMaxItems());
+    }
+
     public MemorizingTrustManager getMemorizingTrustManager() {
         return this.mMemorizingTrustManager;
     }
@@ -5109,15 +5266,15 @@ public class XmppConnectionService extends Service {
         this.mMemorizingTrustManager = trustManager;
     }
 
-    public void updateMemorizingTrustmanager() {
-        final MemorizingTrustManager tm;
-        final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas);
-        if (dontTrustSystemCAs) {
-            tm = new MemorizingTrustManager(getApplicationContext(), null);
+    public void updateMemorizingTrustManager() {
+        final MemorizingTrustManager trustManager;
+        final var appSettings = new AppSettings(this);
+        if (appSettings.isTrustSystemCAStore()) {
+            trustManager = new MemorizingTrustManager(getApplicationContext());
         } else {
-            tm = new MemorizingTrustManager(getApplicationContext());
+            trustManager = new MemorizingTrustManager(getApplicationContext(), null);
         }
-        setMemorizingTrustManager(tm);
+        setMemorizingTrustManager(trustManager);
     }
 
     public LruCache<String, Drawable> getDrawableCache() {
@@ -5140,9 +5297,6 @@ public class XmppConnectionService extends Service {
         if (Config.QUICKSY_DOMAIN != null) {
             hosts.remove(Config.QUICKSY_DOMAIN.toEscapedString()); //we only want to show this when we type a e164 number
         }
-        if (Config.DOMAIN_LOCK != null) {
-            hosts.add(Config.DOMAIN_LOCK);
-        }
         if (Config.MAGIC_CREATE_DOMAIN != null) {
             hosts.add(Config.MAGIC_CREATE_DOMAIN);
         }
@@ -5597,7 +5751,7 @@ public class XmppConnectionService extends Service {
         return templates;
     }
 
-    public void saveConversationAsBookmark(Conversation conversation, String name) {
+    public void saveConversationAsBookmark(final Conversation conversation, final String name) {
         final Account account = conversation.getAccount();
         final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
         String nick = conversation.getMucOptions().getActualNick();
@@ -5608,7 +5762,7 @@ public class XmppConnectionService extends Service {
         if (!TextUtils.isEmpty(name)) {
             bookmark.setBookmarkName(name);
         }
-        bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin)));
+        bookmark.setAutojoin(true);
         createBookmark(account, bookmark);
         bookmark.setConversation(conversation);
     }
@@ -5656,7 +5810,7 @@ public class XmppConnectionService extends Service {
     }
 
     public boolean blindTrustBeforeVerification() {
-        return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
+        return getBooleanPreference(AppSettings.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
     }
 
     public ShortcutService getShortcutService() {
@@ -5727,7 +5881,7 @@ public class XmppConnectionService extends Service {
     public interface OnJingleRtpConnectionUpdate {
         void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state);
 
-        void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
+        void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices);
     }
 
     public interface OnAccountUpdate {

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

@@ -1,31 +1,25 @@
 package eu.siacs.conversations.ui;
 
+import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;
+
 import android.os.Bundle;
 
-import androidx.appcompat.app.AppCompatActivity;
+import androidx.databinding.DataBindingUtil;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.ui.util.SettingsUtils;
-import eu.siacs.conversations.utils.ThemeHelper;
-
-import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;
+import eu.siacs.conversations.databinding.ActivityAboutBinding;
 
-public class AboutActivity extends AppCompatActivity {
+public class AboutActivity extends ActionBarActivity {
 
-    @Override
-    protected void onResume(){
-        super.onResume();
-        SettingsUtils.applyScreenshotPreventionSetting(this);
-    }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        setTheme(ThemeHelper.find(this));
+        final ActivityAboutBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_about);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 
-        setContentView(R.layout.activity_about);
-        setSupportActionBar(findViewById(R.id.toolbar));
+        setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
         setTitle(getString(R.string.title_activity_about_x, getString(R.string.app_name)));
     }

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

@@ -2,26 +2,28 @@ package eu.siacs.conversations.ui;
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.preference.Preference;
 import android.util.AttributeSet;
 
+import com.google.common.base.Strings;
+
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.utils.PhoneHelper;
 
 public class AboutPreference extends Preference {
-	public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) {
-		super(context, attrs, defStyle);
+    public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) {
+        super(context, attrs, defStyle);
         setSummaryAndTitle(context);
-	}
+    }
 
-	public AboutPreference(final Context context, final AttributeSet attrs) {
-		super(context, attrs);
-		setSummaryAndTitle(context);
-	}
+    public AboutPreference(final Context context, final AttributeSet attrs) {
+        super(context, attrs);
+        setSummaryAndTitle(context);
+    }
 
-	private void setSummaryAndTitle(final Context context) {
-	    setSummary(String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
+    private void setSummaryAndTitle(final Context context) {
+        setSummary(String.format("%s %s %s (%s)", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME, im.conversations.webrtc.BuildConfig.WEBRTC_VERSION, Strings.nullToEmpty(Build.DEVICE)));
         setTitle(context.getString(R.string.title_activity_about_x, BuildConfig.APP_NAME));
     }
 
@@ -32,4 +34,3 @@ public class AboutPreference extends Preference {
         getContext().startActivity(intent);
     }
 }
-

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

@@ -14,6 +14,7 @@ import android.widget.EditText;
 import android.widget.ListView;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.databinding.DataBindingUtil;
 
 import java.util.ArrayList;
@@ -35,7 +36,7 @@ public abstract class AbstractSearchableListItemActivity extends XmppActivity im
 	private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
 
 		@Override
-		public boolean onMenuItemActionExpand(final MenuItem item) {
+		public boolean onMenuItemActionExpand(@NonNull final MenuItem item) {
 			mSearchEditText.post(() -> {
 				mSearchEditText.requestFocus();
 				final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -46,7 +47,7 @@ public abstract class AbstractSearchableListItemActivity extends XmppActivity im
 		}
 
 		@Override
-		public boolean onMenuItemActionCollapse(final MenuItem item) {
+		public boolean onMenuItemActionCollapse(@NonNull final MenuItem item) {
 			final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 			imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
 			mSearchEditText.setText("");
@@ -93,6 +94,7 @@ public abstract class AbstractSearchableListItemActivity extends XmppActivity im
 	public void onCreate(final Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		this.binding = DataBindingUtil.setContentView(this,R.layout.activity_choose_contact);
+		Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 		setSupportActionBar(binding.toolbar);
 		configureActionBar(getSupportActionBar());
 		this.binding.chooseContactList.setFastScrollEnabled(true);
@@ -125,7 +127,7 @@ public abstract class AbstractSearchableListItemActivity extends XmppActivity im
 	protected abstract void filterContacts(final String needle);
 
 	@Override
-	void onBackendConnected() {
+    protected void onBackendConnected() {
 		filterContacts();
 	}
 

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

@@ -1,14 +1,10 @@
 package eu.siacs.conversations.ui;
 
 import android.view.MenuItem;
-import android.view.View;
 
 import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
 
-
-public abstract class ActionBarActivity extends AppCompatActivity {
+public abstract class ActionBarActivity extends BaseActivity {
     public static void configureActionBar(ActionBar actionBar) {
         configureActionBar(actionBar, true);
     }
@@ -20,17 +16,11 @@ public abstract class ActionBarActivity extends AppCompatActivity {
         }
     }
 
-    public void setSupportActionBar(View toolbar) {
-        super.setSupportActionBar((Toolbar) toolbar);
-    }
-
     @Override
     public boolean onOptionsItemSelected(final MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                finish();
-                break;
+        if (item.getItemId() == android.R.id.home) {
+            finish();
         }
         return super.onOptionsItemSelected(item);
     }
-}
+}

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

@@ -0,0 +1,52 @@
+package eu.siacs.conversations.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.view.View;
+import com.google.android.material.elevation.SurfaceColors;
+
+public final class Activities {
+
+    private Activities() {}
+
+    public static void setStatusAndNavigationBarColors(final Activity activity, final View view) {
+        setStatusAndNavigationBarColors(activity, view, false);
+    }
+
+    public static void setStatusAndNavigationBarColors(
+            final Activity activity, final View view, final boolean raisedStatusBar) {
+        final var isLightMode = isLightMode(activity);
+        final var window = activity.getWindow();
+        final var flags = view.getSystemUiVisibility();
+        // an elevation of 4 matches the MaterialToolbar elevation
+        if (raisedStatusBar) {
+            window.setStatusBarColor(SurfaceColors.SURFACE_5.getColor(activity));
+        } else {
+            window.setStatusBarColor(SurfaceColors.SURFACE_0.getColor(activity));
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            window.setNavigationBarColor(SurfaceColors.SURFACE_1.getColor(activity));
+            if (isLightMode) {
+                view.setSystemUiVisibility(
+                        flags
+                                | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+                                | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+            }
+        } else if (isLightMode) {
+            view.setSystemUiVisibility(flags | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+        }
+    }
+
+    private static boolean isLightMode(final Context context) {
+        final int nightModeFlags =
+                context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+        return nightModeFlags != Configuration.UI_MODE_NIGHT_YES;
+    }
+
+    public static boolean isNightMode(final Context context) {
+        return (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+                == Configuration.UI_MODE_NIGHT_YES;
+    }
+}

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

@@ -0,0 +1,53 @@
+package eu.siacs.conversations.ui;
+
+import android.util.Log;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
+
+import eu.siacs.conversations.Conversations;
+import eu.siacs.conversations.ui.util.SettingsUtils;
+
+public abstract class BaseActivity extends AppCompatActivity {
+    private Boolean isDynamicColors;
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        final int desiredNightMode = Conversations.getDesiredNightMode(this);
+        if (setDesiredNightMode(desiredNightMode)) {
+            return;
+        }
+        final boolean isDynamicColors = Conversations.isDynamicColorsDesired(this);
+        setDynamicColors(isDynamicColors);
+    }
+
+    @Override
+    protected void onResume(){
+        super.onResume();
+        SettingsUtils.applyScreenshotSetting(this);
+    }
+
+    public void setDynamicColors(final boolean isDynamicColors) {
+        if (this.isDynamicColors == null) {
+            this.isDynamicColors = isDynamicColors;
+        } else {
+            if (this.isDynamicColors != isDynamicColors) {
+                Log.i(
+                        "Recreating {} because dynamic color setting has changed",
+                        getClass().getSimpleName());
+                recreate();
+            }
+        }
+    }
+
+    public boolean setDesiredNightMode(final int desiredNightMode) {
+        if (desiredNightMode == AppCompatDelegate.getDefaultNightMode()) {
+            return false;
+        }
+        AppCompatDelegate.setDefaultNightMode(desiredNightMode);
+        Log.i("Recreating {} because desired night mode has changed", getClass().getSimpleName());
+        recreate();
+        return true;
+    }
+}

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

@@ -7,6 +7,8 @@ import androidx.annotation.StringRes;
 import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.DialogBlockContactBinding;
 import eu.siacs.conversations.entities.Blockable;
@@ -19,7 +21,7 @@ public final class BlockContactDialog {
 		show(xmppActivity, blockable, null);
 	}
 	public static void show(final XmppActivity xmppActivity, final Blockable blockable, final String serverMsgId) {
-		final AlertDialog.Builder builder = new AlertDialog.Builder(xmppActivity);
+		final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(xmppActivity);
 		final boolean isBlocked = blockable.isBlocked();
 		builder.setNegativeButton(R.string.cancel, null);
 		DialogBlockContactBinding binding = DataBindingUtil.inflate(xmppActivity.getLayoutInflater(), R.layout.dialog_block_contact, null, false);
@@ -70,7 +72,7 @@ public final class BlockContactDialog {
 			} else {
 				boolean toastShown = false;
 				if (xmppActivity.xmppConnectionService.sendBlockRequest(blockable, binding.reportSpam.isChecked(), serverMsgId)) {
-					Toast.makeText(xmppActivity, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show();
+					Toast.makeText(xmppActivity, R.string.corresponding_chats_closed, Toast.LENGTH_SHORT).show();
 					toastShown = true;
 				}
 				if (xmppActivity instanceof ContactDetailsActivity) {

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

@@ -90,7 +90,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
 		dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
 			Blockable blockable = new RawBlockable(account, contactJid);
 			if (xmppConnectionService.sendBlockRequest(blockable, false, null)) {
-				Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show();
+				Toast.makeText(BlocklistActivity.this, R.string.corresponding_chats_closed, Toast.LENGTH_SHORT).show();
 			}
 			return true;
 		});

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

@@ -3,88 +3,86 @@ package eu.siacs.conversations.ui;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
-import android.widget.Button;
-import android.widget.EditText;
 import android.widget.Toast;
 
+import androidx.databinding.DataBindingUtil;
+
 import com.google.android.material.textfield.TextInputLayout;
 
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityChangePasswordBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.widget.DisabledActionModeCallback;
 
 public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged {
 
-	private Button mChangePasswordButton;
+	private ActivityChangePasswordBinding binding;
+
 	private final View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() {
 		@Override
-		public void onClick(View view) {
-			if (mAccount != null) {
-				final String currentPassword = mCurrentPassword.getText().toString();
-				final String newPassword = mNewPassword.getText().toString();
-				if (!(mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || didUnlock) && !currentPassword.equals(mAccount.getPassword())) {
-					mCurrentPassword.requestFocus();
-					mCurrentPasswordLayout.setError(getString(R.string.account_status_unauthorized));
-					removeErrorsOnAllBut(mCurrentPasswordLayout);
-				} else if (newPassword.trim().isEmpty()) {
-					mNewPassword.requestFocus();
-					mNewPasswordLayout.setError(getString(R.string.password_should_not_be_empty));
-					removeErrorsOnAllBut(mNewPasswordLayout);
-				} else {
-					mCurrentPasswordLayout.setError(null);
-					mNewPasswordLayout.setError(null);
-					xmppConnectionService.updateAccountPasswordOnServer(mAccount, newPassword, ChangePasswordActivity.this);
-					mChangePasswordButton.setEnabled(false);
-					mChangePasswordButton.setText(R.string.updating);
-				}
+		public void onClick(final View view) {
+			final var account = mAccount;
+			if (account == null) {
+				return;
+			}
+			final String currentPassword = binding.currentPassword.getText().toString();
+			final String newPassword = binding.newPassword.getText().toString();
+			if ((!account.isOptionSet(Account.OPTION_MAGIC_CREATE) || didUnlock) && !currentPassword.equals(account.getPassword())) {
+				binding.currentPassword.requestFocus();
+				binding.currentPasswordLayout.setError(getString(R.string.account_status_unauthorized));
+				removeErrorsOnAllBut(binding.currentPasswordLayout);
+			} else if (newPassword.trim().isEmpty()) {
+				binding.newPassword.requestFocus();
+				binding.newPasswordLayout.setError(getString(R.string.password_should_not_be_empty));
+				removeErrorsOnAllBut(binding.newPasswordLayout);
+			} else {
+				binding.currentPasswordLayout.setError(null);
+				binding.newPasswordLayout.setError(null);
+				xmppConnectionService.updateAccountPasswordOnServer(account, newPassword, ChangePasswordActivity.this);
+				binding.changePasswordButton.setEnabled(false);
+				binding.changePasswordButton.setText(R.string.updating);
 			}
 		}
 	};
-	private EditText mCurrentPassword;
-	private EditText mNewPassword;
-	private TextInputLayout mNewPasswordLayout;
-	private TextInputLayout mCurrentPasswordLayout;
+
+
+
 	private Account mAccount;
 	private boolean didUnlock = false;
 
 	@Override
-	void onBackendConnected() {
+    protected void onBackendConnected() {
 		this.mAccount = extractAccount(getIntent());
 		if (this.mAccount != null && (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || didUnlock)) {
-			this.mCurrentPasswordLayout.setVisibility(View.GONE);
+			this.binding.currentPasswordLayout.setVisibility(View.GONE);
 		} else {
-			this.mCurrentPassword.setVisibility(View.VISIBLE);
+			this.binding.currentPasswordLayout.setVisibility(View.VISIBLE);
 		}
 	}
 
 	@Override
 	protected void onCreate(final Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
-		setContentView(R.layout.activity_change_password);
-		setSupportActionBar(findViewById(R.id.toolbar));
+		this.binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password);
+		Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+		setSupportActionBar(binding.toolbar);
 		configureActionBar(getSupportActionBar());
-		Button mCancelButton = findViewById(R.id.left_button);
-		mCancelButton.setOnClickListener(view -> finish());
-		this.mChangePasswordButton = findViewById(R.id.right_button);
-		this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
-		this.mCurrentPassword = findViewById(R.id.current_password);
-		this.mCurrentPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
-		this.mNewPassword = findViewById(R.id.new_password);
-		this.mNewPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
-		this.mCurrentPasswordLayout = findViewById(R.id.current_password_layout);
-		this.mNewPasswordLayout = findViewById(R.id.new_password_layout);
+		binding.cancelButton.setOnClickListener(view -> finish());
+		binding.changePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
+		binding.currentPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
+		binding.newPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
 	}
 
 	@Override
-	protected void onStart() {
+	public void onStart() {
 		super.onStart();
 		Intent intent = getIntent();
 		this.didUnlock = intent.getBooleanExtra("did_unlock", false);
 		String password = intent != null ? intent.getStringExtra("password") : null;
 		if (password != null) {
-			this.mNewPassword.getEditableText().clear();
-			this.mNewPassword.getEditableText().append(password);
+			binding.newPassword.getEditableText().clear();
+			binding.newPassword.getEditableText().append(password);
 		}
 	}
 
@@ -99,21 +97,21 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
 	@Override
 	public void onPasswordChangeFailed() {
 		runOnUiThread(() -> {
-			mNewPasswordLayout.setError(getString(R.string.could_not_change_password));
-			mChangePasswordButton.setEnabled(true);
-			mChangePasswordButton.setText(R.string.change_password);
+			binding.newPasswordLayout.setError(getString(R.string.could_not_change_password));
+			binding.changePasswordButton.setEnabled(true);
+			binding.changePasswordButton.setText(R.string.change_password);
 		});
 
 	}
 
 	private void removeErrorsOnAllBut(TextInputLayout exception) {
-		if (this.mCurrentPasswordLayout != exception) {
-			this.mCurrentPasswordLayout.setErrorEnabled(false);
-			this.mCurrentPasswordLayout.setError(null);
+		if (this.binding.currentPasswordLayout != exception) {
+			this.binding.currentPasswordLayout.setErrorEnabled(false);
+			this.binding.currentPasswordLayout.setError(null);
 		}
-		if (this.mNewPasswordLayout != exception) {
-			this.mNewPasswordLayout.setErrorEnabled(false);
-			this.mNewPasswordLayout.setError(null);
+		if (this.binding.newPasswordLayout != exception) {
+			this.binding.newPasswordLayout.setErrorEnabled(false);
+			this.binding.newPasswordLayout.setError(null);
 		}
 
 	}

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

@@ -18,8 +18,12 @@ import android.widget.EditText;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.common.base.Strings;
 
 import java.util.Collections;
@@ -39,7 +43,6 @@ import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.ui.adapter.ChannelSearchResultAdapter;
 import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.xmpp.Jid;
 
@@ -65,7 +68,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         if (pendingServices != null) {
             mucServices = new HashMap<>();
             for (int i = 0; i < pendingServices.length; i += 2) {
@@ -92,6 +95,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
         super.onCreate(savedInstanceState);
         binding = DataBindingUtil.setContentView(this, R.layout.activity_channel_discovery);
         setSupportActionBar(binding.toolbar);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         configureActionBar(getSupportActionBar(), true);
         binding.list.setAdapter(this.adapter);
         this.adapter.setOnChannelSearchResultSelectedListener(this);
@@ -145,7 +149,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
     }
 
     @Override
-    public boolean onMenuItemActionExpand(MenuItem item) {
+    public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
         mSearchEditText.post(() -> {
             mSearchEditText.requestFocus();
             final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -155,7 +159,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
     }
 
     @Override
-    public boolean onMenuItemActionCollapse(MenuItem item) {
+    public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
         final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
         imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
         mSearchEditText.setText("");
@@ -169,7 +173,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
     private void toggleLoadingScreen() {
         adapter.submitList(Collections.emptyList());
         binding.progressBar.setVisibility(View.VISIBLE);
-        binding.list.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary));
+        binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface));
     }
 
     @Override
@@ -177,13 +181,13 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
         super.onStart();
         this.method = getMethod(this);
         if (pendingServices == null && !optedIn && method == ChannelDiscoveryService.Method.JABBER_NETWORK) {
-            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
             builder.setTitle(R.string.channel_discovery_opt_in_title);
             builder.setMessage(Html.fromHtml(getString(R.string.channel_discover_opt_in_message)));
             builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
             builder.setPositiveButton(R.string.confirm, (dialog, which) -> optIn());
             builder.setOnCancelListener(dialog -> finish());
-            final AlertDialog dialog = builder.create();
+            final androidx.appcompat.app.AlertDialog dialog = builder.create();
             dialog.setOnShowListener(d -> {
                 final TextView textView = dialog.findViewById(android.R.id.message);
                 if (textView == null) {
@@ -200,11 +204,11 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
     private void holdLoading() {
         adapter.submitList(Collections.emptyList());
         binding.progressBar.setVisibility(View.GONE);
-        binding.list.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary));
+        binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface));
     }
 
     @Override
-    public void onSaveInstanceState(Bundle savedInstanceState) {
+    public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
         if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
             savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null);
         }
@@ -234,10 +238,10 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
         runOnUiThread(() -> {
             adapter.submitList(results);
             binding.progressBar.setVisibility(View.GONE);
-            if (results.size() == 0) {
-                binding.list.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_primary_background_no_results));
+            if (results.isEmpty()) {
+                binding.list.setBackground(ContextCompat.getDrawable(this,R.drawable.background_no_results));
             } else {
-                binding.list.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary));
+                binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface));
             }
         });
 
@@ -248,11 +252,11 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
         final List<String> accounts = AccountUtils.getEnabledAccounts(xmppConnectionService);
         if (accounts.size() == 1) {
             joinChannelSearchResult(accounts.get(0), result);
-        } else if (accounts.size() == 0) {
+        } else if (accounts.isEmpty()) {
             Toast.makeText(this, R.string.please_enable_an_account, Toast.LENGTH_LONG).show();
         } else {
             final AtomicReference<String> account = new AtomicReference<>(accounts.get(0));
-            AlertDialog.Builder builder = new AlertDialog.Builder(this);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
             builder.setTitle(R.string.choose_account);
             builder.setSingleChoiceItems(accounts.toArray(new CharSequence[0]), 0, (dialog, which) -> account.set(accounts.get(which)));
             builder.setPositiveButton(R.string.join, (dialog, which) -> joinChannelSearchResult(account.get(), result));
@@ -263,40 +267,43 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
     }
 
     @Override
-    public boolean onContextItemSelected(MenuItem item) {
+    public boolean onContextItemSelected(@NonNull MenuItem item) {
         final Room room = adapter.getCurrent();
-        if (room != null) {
-            switch (item.getItemId()) {
-                case R.id.share_with:
-                    StartConversationActivity.shareAsChannel(this, room.address);
-                    return true;
-                case R.id.open_join_dialog:
-                    final Intent intent = new Intent(this, StartConversationActivity.class);
-                    intent.setAction(Intent.ACTION_VIEW);
-                    intent.putExtra("force_dialog", true);
-                    intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address)));
-                    startActivity(intent);
-                    return true;
-            }
+        if (room == null) {
+            return false;
+        }
+        final int itemId = item.getItemId();
+        if (itemId == R.id.share_with) {
+            StartConversationActivity.shareAsChannel(this, room.address);
+            return true;
+        } else if (itemId == R.id.open_join_dialog) {
+            final Intent intent = new Intent(this, StartConversationActivity.class);
+            intent.setAction(Intent.ACTION_VIEW);
+            intent.putExtra("force_dialog", true);
+            intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address)));
+            startActivity(intent);
+            return true;
+        } else {
+            return false;
         }
-        return false;
     }
 
-    public void joinChannelSearchResult(String selectedAccount, Room result) {
-        final Jid jid = Config.DOMAIN_LOCK == null ? Jid.ofEscaped(selectedAccount) : Jid.ofLocalAndDomainEscaped(selectedAccount, Config.DOMAIN_LOCK);
-        final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin);
+    public void joinChannelSearchResult(final String selectedAccount, final Room result) {
+        final Jid jid = Jid.ofEscaped(selectedAccount);
         final Account account = xmppConnectionService.findAccountByJid(jid);
-        final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true);
-        Bookmark bookmark = conversation.getBookmark();
-        if (bookmark != null) {
-            if (!bookmark.autojoin() && syncAutoJoin) {
-                bookmark.setAutojoin(true);
-                xmppConnectionService.createBookmark(account, bookmark);
-            }
-        } else {
-            bookmark = new Bookmark(account, conversation.getJid().asBareJid());
-            bookmark.setAutojoin(syncAutoJoin);
+        final Conversation conversation =
+                xmppConnectionService.findOrCreateConversation(
+                        account, result.getRoom(), true, true, true);
+        final var existingBookmark = conversation.getBookmark();
+        if (existingBookmark == null) {
+            final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
+            bookmark.setAutojoin(true);
             xmppConnectionService.createBookmark(account, bookmark);
+        } else {
+            if (!existingBookmark.autojoin()) {
+                existingBookmark.setAutojoin(true);
+                xmppConnectionService.createBookmark(account, existingBookmark);
+            }
         }
         switchToConversation(conversation);
     }

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

@@ -3,20 +3,21 @@ package eu.siacs.conversations.ui;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
-import android.widget.ListView;
 import android.widget.Toast;
 
-import java.util.ArrayList;
-import java.util.List;
+import androidx.databinding.DataBindingUtil;
 
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityManageAccountsBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.ui.adapter.AccountAdapter;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class ChooseAccountForProfilePictureActivity extends XmppActivity {
 
     protected final List<Account> accountList = new ArrayList<>();
-    protected ListView accountListView;
     protected AccountAdapter mAccountAdapter;
 
     @Override
@@ -28,29 +29,25 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_manage_accounts);
-        setSupportActionBar(findViewById(R.id.toolbar));
+        final ActivityManageAccountsBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_manage_accounts);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+        setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar(), false);
-        accountListView = findViewById(R.id.account_list);
         this.mAccountAdapter = new AccountAdapter(this, accountList, false);
-        accountListView.setAdapter(this.mAccountAdapter);
-        accountListView.setOnItemClickListener((arg0, view, position, arg3) -> {
+        binding.accountList.setAdapter(this.mAccountAdapter);
+        binding.accountList.setOnItemClickListener((arg0, view, position, arg3) -> {
             final Account account = accountList.get(position);
             goToProfilePictureActivity(account);
         });
     }
 
     @Override
-    protected void onStart() {
+    public void onStart() {
         super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         loadEnabledAccounts();
         if (accountList.size() == 1) {
             goToProfilePictureActivity(accountList.get(0));

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

@@ -9,6 +9,7 @@ import android.view.ActionMode;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.SoundEffectConstants;
 import android.view.View;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AbsListView.MultiChoiceModeListener;
@@ -51,7 +52,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
     public static final String EXTRA_SHOW_ENTER_JID = "extra_show_enter_jid";
     public static final String EXTRA_CONVERSATION = "extra_conversation";
     private static final String EXTRA_FILTERED_CONTACTS = "extra_filtered_contacts";
-    private final List<String> mActivatedAccounts = new ArrayList<>();
+    private final ArrayList<String> mActivatedAccounts = new ArrayList<>();
     private final Set<String> selected = new HashSet<>();
     private Set<String> filterContacts;
 
@@ -130,7 +131,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
         if (this.showEnterJid) {
             this.binding.fab.show();
         } else {
-            binding.fab.setImageResource(R.drawable.ic_forward_white_24dp);
+            binding.fab.setImageResource(R.drawable.ic_navigate_next_24dp);
         }
 
         final SharedPreferences preferences = getPreferences();
@@ -148,7 +149,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
     }
 
     private void onFabClicked(View v) {
-        if (selected.size() == 0) {
+        if (selected.isEmpty()) {
             showEnterJidDialog(null);
         } else {
             submitSelection();
@@ -163,7 +164,8 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
     @Override
     public boolean onCreateActionMode(ActionMode mode, Menu menu) {
         mode.setTitle(getTitleFromIntent());
-        binding.fab.setImageResource(R.drawable.ic_forward_white_24dp);
+        binding.chooseContactList.setFastScrollEnabled(false);
+        binding.fab.setImageResource(R.drawable.ic_navigate_next_24dp);
         binding.fab.show();
         final View view = getSearchEditText();
         final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -175,12 +177,13 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
 
     @Override
     public void onDestroyActionMode(ActionMode mode) {
-        this.binding.fab.setImageResource(R.drawable.ic_person_add_white_24dp);
+        this.binding.fab.setImageResource(R.drawable.ic_person_add_24dp);
         if (this.showEnterJid) {
             this.binding.fab.show();
         } else {
             this.binding.fab.hide();
         }
+        binding.chooseContactList.setFastScrollEnabled(true);
         selected.clear();
     }
 
@@ -208,8 +211,9 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
     @Override
     public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
         if (selected.size() != 0) {
-            getListView().playSoundEffect(0);
+            getListView().playSoundEffect(SoundEffectConstants.CLICK);
         }
+        getListItemAdapter().notifyDataSetChanged();
         Contact item = (Contact) getListItems().get(position);
         if (checked) {
             selected.add(item.getJid().toString());
@@ -378,16 +382,12 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         filterContacts();
         this.mActivatedAccounts.clear();
-        for (Account account : xmppConnectionService.getAccounts()) {
+        for (final Account account : xmppConnectionService.getAccounts()) {
             if (account.isEnabled()) {
-                if (Config.DOMAIN_LOCK != null) {
-                    this.mActivatedAccounts.add(account.getJid().getEscapedLocal());
-                } else {
-                    this.mActivatedAccounts.add(account.getJid().asBareJid().toEscapedString());
-                }
+                this.mActivatedAccounts.add(account.getJid().asBareJid().toEscapedString());
             }
         }
         ActivityResult activityResult = this.postponedActivityResult.pop();
@@ -416,14 +416,14 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
             }
 
             if (selected.isEmpty()) {
-                this.binding.fab.setImageResource(R.drawable.ic_person_add_white_24dp);
+                this.binding.fab.setImageResource(R.drawable.ic_person_add_24dp);
                 if (this.showEnterJid) {
                     this.binding.fab.show();
                 } else {
                     this.binding.fab.hide();
                 }
             } else {
-                binding.fab.setImageResource(R.drawable.ic_forward_white_24dp);
+                binding.fab.setImageResource(R.drawable.ic_navigate_next_24dp);
                 binding.fab.show();
             }
 

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

@@ -5,7 +5,7 @@ import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.graphics.PorterDuff;
+import android.content.res.ColorStateList;
 import android.net.Uri;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
@@ -25,10 +25,15 @@ import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
+import androidx.core.view.ViewCompat;
 import androidx.databinding.DataBindingUtil;
 
 import com.cheogram.android.Util;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -68,6 +73,7 @@ import eu.siacs.conversations.utils.StringUtils;
 import eu.siacs.conversations.utils.StylingHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.utils.XmppUri;
+import eu.siacs.conversations.utils.XEP0392Helper;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import me.drakeet.support.toast.ToastCompat;
@@ -75,6 +81,8 @@ import me.drakeet.support.toast.ToastCompat;
 import static eu.siacs.conversations.entities.Bookmark.printableValue;
 import static eu.siacs.conversations.utils.StringUtils.changed;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnConfigurationPushed, XmppConnectionService.OnRoomDestroy, TextWatcher, OnMediaLoaded {
     public static final String ACTION_VIEW_MUC = "view_muc";
 
@@ -118,7 +126,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     private final OnClickListener mNotifyStatusClickListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
-            AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
             builder.setTitle(R.string.pref_notification_settings);
             String[] choices = {
                     getString(R.string.notify_on_all_messages),
@@ -153,7 +161,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         @Override
         public void onClick(View v) {
             final MucOptions mucOptions = mConversation.getMucOptions();
-            AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
             MucConfiguration configuration = MucConfiguration.get(ConferenceDetailsActivity.this, mAdvancedMode, mucOptions);
             builder.setTitle(configuration.title);
             final boolean[] values = configuration.values;
@@ -192,7 +200,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         super.onCreate(savedInstanceState);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_details);
         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
-        showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, getResources().getBoolean(R.bool.show_dynamic_tags));
+        showDynamicTags = preferences.getBoolean("show_dynamic_tags", getResources().getBoolean(R.bool.show_dynamic_tags));
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings);
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
@@ -251,12 +260,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     }
 
     @Override
-    protected void onStart() {
+    public void onStart() {
         super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        }
         binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
     }
 
@@ -278,9 +283,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             case R.id.action_save_as_bookmark:
                 saveAsBookmark();
                 break;
-            case R.id.action_delete_bookmark:
-                deleteBookmark();
-                break;
             case R.id.action_destroy_room:
                 destroyRoom();
                 break;
@@ -315,7 +317,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             final MucOptions mucOptions = mConversation.getMucOptions();
             this.binding.mucEditor.setVisibility(View.VISIBLE);
             this.binding.mucDisplay.setVisibility(View.GONE);
-            this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_cancel, R.drawable.ic_cancel_black_24dp));
+            this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
             final String name = mucOptions.getName();
             this.binding.mucEditTitle.setText("");
             final boolean owner = mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
@@ -389,7 +391,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     private void hideEditor() {
         this.binding.mucEditor.setVisibility(View.GONE);
         this.binding.mucDisplay.setVisibility(View.VISIBLE);
-        this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_edit_body, R.drawable.ic_edit_black_24dp));
+        this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp);
     }
 
     private void onMucInfoUpdated(String subject, String name) {
@@ -420,28 +422,21 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     }
 
     @Override
-    public boolean onPrepareOptionsMenu(Menu menu) {
-        MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark);
-        MenuItem menuItemDeleteBookmark = menu.findItem(R.id.action_delete_bookmark);
-        MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode);
-        MenuItem menuItemDestroyRoom = menu.findItem(R.id.action_destroy_room);
+    public boolean onPrepareOptionsMenu(final Menu menu) {
+        final MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark);
+        final MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode);
+        final MenuItem menuItemDestroyRoom = menu.findItem(R.id.action_destroy_room);
         menuItemAdvancedMode.setChecked(mAdvancedMode);
         if (mConversation == null) {
             return true;
         }
-        if (mConversation.getBookmark() != null) {
-            menuItemSaveBookmark.setVisible(false);
-            menuItemDeleteBookmark.setVisible(true);
-        } else {
-            menuItemDeleteBookmark.setVisible(false);
-            menuItemSaveBookmark.setVisible(true);
-        }
+        menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
         menuItemDestroyRoom.setVisible(mConversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER));
         return true;
     }
 
     @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
+    public boolean onCreateOptionsMenu(final Menu menu) {
         final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
         getMenuInflater().inflate(R.menu.muc_details, menu);
         final MenuItem share = menu.findItem(R.id.action_share);
@@ -467,17 +462,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName());
     }
 
-    protected void deleteBookmark() {
-        final Account account = mConversation.getAccount();
-        final Bookmark bookmark = mConversation.getBookmark();
-        bookmark.setConversation(null);
-        xmppConnectionService.deleteBookmark(account, bookmark);
-        updateView();
-    }
-
     protected void destroyRoom() {
         final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
         builder.setMessage(groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
         builder.setPositiveButton(R.string.ok, (dialog, which) -> {
@@ -490,7 +477,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         if (mPendingConferenceInvite != null) {
             mPendingConferenceInvite.execute(this);
             mPendingConferenceInvite = null;
@@ -527,12 +514,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
         final MucOptions mucOptions = mConversation.getMucOptions();
         final User self = mucOptions.getSelf();
-        String account;
-        if (Config.DOMAIN_LOCK != null) {
-            account = mConversation.getAccount().getJid().getEscapedLocal();
-        } else {
-            account = mConversation.getAccount().getJid().asBareJid().toEscapedString();
-        }
+        final String account = mConversation.getAccount().getJid().asBareJid().toEscapedString();
         setTitle(mucOptions.isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details);
         final Bookmark bookmark = mConversation.getBookmark();
         final XmppConnection connection = mConversation.getAccount().getXmppConnection();
@@ -567,7 +549,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
             MyLinkify.addLinks(spannable, false);
             this.binding.mucSubject.setText(spannable);
-            this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead);
+            this.binding.mucSubject.setTextAppearance( subject.length() > (hasTitle ? 128 : 196) ? com.google.android.material.R.style.TextAppearance_Material3_BodyMedium : com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
             this.binding.mucSubject.setAutoLinkMask(0);
             this.binding.mucSubject.setVisibility(View.VISIBLE);
             this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
@@ -605,27 +587,22 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             this.binding.mucSettings.setVisibility(View.GONE);
         }
 
-        int ic_notifications = getThemeResource(R.attr.icon_notifications, R.drawable.ic_notifications_black_24dp);
-        int ic_notifications_off = getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp);
-        int ic_notifications_paused = getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp);
-        int ic_notifications_none = getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp);
-
-        long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
+        final long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
         if (mutedTill == Long.MAX_VALUE) {
             this.binding.notificationStatusText.setText(R.string.notify_never);
-            this.binding.notificationStatusButton.setImageResource(ic_notifications_off);
+            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_off_24dp);
         } else if (System.currentTimeMillis() < mutedTill) {
             this.binding.notificationStatusText.setText(R.string.notify_paused);
-            this.binding.notificationStatusButton.setImageResource(ic_notifications_paused);
+            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_paused_24dp);
         } else if (mConversation.alwaysNotify()) {
             this.binding.notificationStatusText.setText(R.string.notify_on_all_messages);
-            this.binding.notificationStatusButton.setImageResource(ic_notifications);
+            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_24dp);
         } else if (mConversation.notifyReplies()) {
             this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted_or_replied);
-            this.binding.notificationStatusButton.setImageResource(ic_notifications_none);
+            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp);
         } else {
             this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted);
-            this.binding.notificationStatusButton.setImageResource(ic_notifications_none);
+            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp);
         }
         final List<User> users = mucOptions.getUsers();
         Collections.sort(users, (a, b) -> {
@@ -671,19 +648,25 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             Util.justifyListViewHeightBasedOnChildren(binding.recentThreads);
         }
 
-        List<ListItem.Tag> tagList = bookmark.getTags(this);
-        if (tagList.size() == 0 || !showDynamicTags) {
+        final List<ListItem.Tag> tagList = bookmark.getTags(this);
+        if (tagList.isEmpty() || !this.showDynamicTags) {
             binding.tags.setVisibility(View.GONE);
         } else {
             final LayoutInflater inflater = getLayoutInflater();
             binding.tags.setVisibility(View.VISIBLE);
-            binding.tags.removeAllViewsInLayout();
+            binding.tags.removeViews(1, binding.tags.getChildCount() - 1);
+            final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
             for (final ListItem.Tag tag : tagList) {
+                final String name = tag.getName();
                 final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
-                tv.setText(tag.getName());
-                tv.getBackground().mutate().setColorFilter(tag.getColor(), PorterDuff.Mode.SRC_IN);
+                tv.setText(name);
+                tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name))));
+                final int id = ViewCompat.generateViewId();
+                tv.setId(id);
+                viewIdBuilder.add(id);
                 binding.tags.addView(tv);
             }
+            binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
         }
     }
 
@@ -760,9 +743,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             boolean nameChanged = changed(binding.mucEditTitle.getEditableText().toString(), mucOptions.getName());
             final Bookmark bookmark = mConversation.getBookmark();
             if (subjectChanged || nameChanged || (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2())) {
-                this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_save, R.drawable.ic_save_black_24dp));
+                this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp);
             } else {
-                this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_cancel, R.drawable.ic_cancel_black_24dp));
+                this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
             }
         }
     }

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

@@ -7,8 +7,8 @@ import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
-import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.content.res.ColorStateList;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -36,10 +36,17 @@ import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
 import androidx.databinding.DataBindingUtil;
 
 import com.cheogram.android.Util;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+
 import org.openintents.openpgp.util.OpenPgpUtils;
 
 import java.util.ArrayList;
@@ -50,6 +57,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -61,6 +69,7 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.services.AbstractQuickConversationsService;
 import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@@ -80,6 +89,7 @@ import eu.siacs.conversations.utils.Emoticons;
 import eu.siacs.conversations.utils.IrregularUnicodeDetector;
 import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.XEP0392Helper;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
@@ -165,7 +175,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         } else {
             value = jid.toEscapedString();
         }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(getString(R.string.action_add_phone_book));
         builder.setMessage(getString(R.string.add_phone_book_text, value));
         builder.setNegativeButton(getString(R.string.cancel), null);
@@ -235,6 +245,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         }
         this.messageFingerprint = getIntent().getStringExtra("fingerprint");
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_contact_details);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
@@ -258,14 +269,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     @Override
     public void onStart() {
         super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        } else {
-            final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
-            this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, getResources().getBoolean(R.bool.show_dynamic_tags));
-            this.showLastSeen = preferences.getBoolean("last_activity", false);
-        }
+        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+        this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
+        this.showLastSeen = preferences.getBoolean("last_activity", false);
         binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
         mMediaAdapter.setAttachments(Collections.emptyList());
     }
@@ -301,8 +307,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         if (MenuDoubleTabUtil.shouldIgnoreTap()) {
             return false;
         }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setNegativeButton(getString(R.string.cancel), null);
         switch (menuItem.getItemId()) {
             case android.R.id.home:
                 finish();
@@ -314,6 +318,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                 shareLink(false);
                 break;
             case R.id.action_delete_contact:
+                final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+                builder.setNegativeButton(getString(R.string.cancel), null);
                 builder.setTitle(getString(R.string.action_delete_contact))
                         .setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()))
                         .setPositiveButton(getString(R.string.delete),
@@ -520,12 +526,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         }
 
         binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
-        String account;
-        if (Config.DOMAIN_LOCK != null) {
-            account = contact.getAccount().getJid().getEscapedLocal();
-        } else {
-            account = contact.getAccount().getJid().asBareJid().toEscapedString();
-        }
+        final String account = contact.getAccount().getJid().asBareJid().toEscapedString();
         binding.detailsAccount.setText(getString(R.string.using_account, account));
         AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
         binding.detailsContactBadge.setOnClickListener(this::onBadgeClick);
@@ -587,7 +588,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             TextView keyType = view.findViewById(R.id.key_type);
             keyType.setText(R.string.openpgp_key_id);
             if ("pgp".equals(messageFingerprint)) {
-                keyType.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
+                keyType.setTextColor(MaterialColors.getColor(keyType, com.google.android.material.R.attr.colorPrimaryVariant));
             }
             key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
             final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId());
@@ -598,18 +599,50 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         }
         binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
 
-        List<ListItem.Tag> tagList = contact.getTags(this);
-        if (tagList.size() == 0 || !this.showDynamicTags) {
+        final List<ListItem.Tag> tagList = contact.getTags(this);
+        final boolean hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
+        if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
             binding.tags.setVisibility(View.GONE);
         } else {
             binding.tags.setVisibility(View.VISIBLE);
-            binding.tags.removeAllViewsInLayout();
+            binding.tags.removeViews(1, binding.tags.getChildCount() - 1);
+            final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
             for (final ListItem.Tag tag : tagList) {
+                final String name = tag.getName();
                 final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
-                tv.setText(tag.getName());
-                tv.getBackground().mutate().setColorFilter(tag.getColor(), PorterDuff.Mode.SRC_IN);
+                tv.setText(name);
+                tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name))));
+                final int id = ViewCompat.generateViewId();
+                tv.setId(id);
+                viewIdBuilder.add(id);
+                binding.tags.addView(tv);
+            }
+            if (contact.isBlocked()) {
+                final TextView tv =
+                        (TextView)
+                                inflater.inflate(
+                                        R.layout.list_item_tag, binding.tags, false);
+                tv.setText(R.string.blocked);
+                tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(), ContextCompat.getColor(tv.getContext(),R.color.gray_800))));
+                final int id = ViewCompat.generateViewId();
+                tv.setId(id);
+                viewIdBuilder.add(id);
                 binding.tags.addView(tv);
+            } else {
+                final Presence.Status status = contact.getShownStatus();
+                if (status != Presence.Status.OFFLINE) {
+                    final TextView tv =
+                            (TextView)
+                                    inflater.inflate(
+                                            R.layout.list_item_tag, binding.tags, false);
+                    UIHelper.setStatus(tv, status);
+                    final int id = ViewCompat.generateViewId();
+                    tv.setId(id);
+                    viewIdBuilder.add(id);
+                    binding.tags.addView(tv);
+                }
             }
+            binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
         }
     }
 
@@ -735,10 +768,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     class VcardAdapter extends ArrayAdapter<Element> {
         VcardAdapter() { super(ContactDetailsActivity.this, 0); }
 
-        private Drawable getDrawable(int attr) {
-            final TypedValue typedvalueattr = new TypedValue();
-            getTheme().resolveAttribute(attr, typedvalueattr, true);
-            return getResources().getDrawable(typedvalueattr.resourceId);
+        private Drawable getDrawable(int d) {
+            return ContactDetailsActivity.this.getDrawable(d);
         }
 
         @Override
@@ -747,13 +778,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             final Element item = getItem(position);
 
             if (item.getName().equals("org")) {
-                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_org), null, null, null);
+                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_business_24dp), null, null, null);
                 binding.command.setCompoundDrawablePadding(20);
             } else if (item.getName().equals("impp")) {
-                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_chat), null, null, null);
+                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_chat_black_24dp), null, null, null);
                 binding.command.setCompoundDrawablePadding(20);
             } else if (item.getName().equals("url")) {
-                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_link), null, null, null);
+                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_link_24dp), null, null, null);
                 binding.command.setCompoundDrawablePadding(20);
             }
 
@@ -761,19 +792,19 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             if (uri != null && uri.getScheme() != null) {
                 if (uri.getScheme().equals("xmpp")) {
                     binding.command.setText(uri.getSchemeSpecificPart());
-                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getResources().getDrawable(R.drawable.jabber), null, null, null);
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.jabber), null, null, null);
                     binding.command.setCompoundDrawablePadding(20);
                 } else if (uri.getScheme().equals("tel")) {
                     binding.command.setText(uri.getSchemeSpecificPart());
-                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.ic_make_audio_call), null, null, null);
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_call_24dp), null, null, null);
                     binding.command.setCompoundDrawablePadding(20);
                 } else if (uri.getScheme().equals("mailto")) {
                     binding.command.setText(uri.getSchemeSpecificPart());
-                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_email), null, null, null);
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_email_24dp), null, null, null);
                     binding.command.setCompoundDrawablePadding(20);
                 } else if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) {
                     binding.command.setText(uri.toString());
-                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_link), null, null, null);
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_link_24dp), null, null, null);
                     binding.command.setCompoundDrawablePadding(20);
                 } else {
                     binding.command.setText(uri.toString());

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

@@ -16,10 +16,4 @@ public class ConversationActivity extends AppCompatActivity {
 		startActivity(new Intent(this, ConversationsActivity.class));
 		finish();
 	}
-
-	@Override
-	protected void onResume(){
-		super.onResume();
-		SettingsUtils.applyScreenshotPreventionSetting(this);
-	}
 }

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

@@ -22,6 +22,7 @@ import android.content.Intent;
 import android.content.IntentSender.SendIntentException;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -83,6 +84,7 @@ import com.cheogram.android.BobTransfer;
 import com.cheogram.android.EmojiSearch;
 import com.cheogram.android.WebxdcPage;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 
@@ -129,6 +131,7 @@ import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.http.HttpDownloadConnection;
 import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.CallIntegrationConnectionService;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService;
@@ -149,7 +152,6 @@ import eu.siacs.conversations.ui.util.ScrollState;
 import eu.siacs.conversations.ui.util.SendButtonAction;
 import eu.siacs.conversations.ui.util.SendButtonTool;
 import eu.siacs.conversations.ui.util.ShareUtil;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.ui.util.ViewUtil;
 import eu.siacs.conversations.ui.widget.EditMessage;
 import eu.siacs.conversations.utils.AccountUtils;
@@ -189,19 +191,7 @@ import java.util.List;
 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;
+import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 
 public class ConversationFragment extends XmppFragment
         implements EditMessage.KeyboardListener,
@@ -1081,9 +1071,11 @@ public class ConversationFragment extends XmppFragment
         } else if (multi && !conversation.getMucOptions().participating()) {
             this.binding.textInputHint.setVisibility(View.GONE);
             this.binding.textinput.setHint(R.string.you_are_not_participating);
+            this.binding.inputLayout.setBackgroundColor(android.R.color.transparent);
         } else {
             this.binding.textInputHint.setVisibility(View.GONE);
             this.binding.textinput.setHint(UIHelper.getMessageHint(activity, conversation));
+            this.binding.inputLayout.setBackground(activity.getDrawable(R.drawable.background_message_bubble));
             activity.invalidateOptionsMenu();
         }
 
@@ -1218,7 +1210,7 @@ public class ConversationFragment extends XmppFragment
                 };
         if (conversation == null
                 || conversation.getMode() == Conversation.MODE_MULTI
-                || Attachment.canBeSendInband(attachments)
+                || Attachment.canBeSendInBand(attachments)
                 || (conversation.getAccount().httpUploadAvailable()
                         && FileBackend.allFilesUnderSize(
                                 getActivity(), attachments, getMaxHttpUploadSize(conversation)))) {
@@ -1586,10 +1578,9 @@ public class ConversationFragment extends XmppFragment
 
         SpannableStringBuilder body = message.getSpannableBody(null, null);
         if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
-        messageListAdapter.handleTextQuotes(body, activity.isDarkTheme());
+        messageListAdapter.handleTextQuotes(binding.contextPreviewText, body);
         binding.contextPreviewText.setText(body);
         binding.contextPreview.setVisibility(View.VISIBLE);
-        binding.textsend.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_primary));
     }
 
     private void setThread(Element thread) {
@@ -2026,23 +2017,28 @@ public class ConversationFragment extends XmppFragment
                         .getOngoingRtpConnection(conversation.getContact());
         if (ongoingRtpSession.isPresent()) {
             final OngoingRtpSession id = ongoingRtpSession.get();
-            final Intent intent = new Intent(activity, RtpSessionActivity.class);
-            intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.getAccount().getJid().asBareJid().toEscapedString());
+            final Intent intent = new Intent(getActivity(), RtpSessionActivity.class);
+            intent.setAction(Intent.ACTION_VIEW);
             intent.putExtra(
                     RtpSessionActivity.EXTRA_ACCOUNT,
                     id.getAccount().getJid().asBareJid().toEscapedString());
             intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.getWith().toEscapedString());
-            if (id instanceof AbstractJingleConnection.Id) {
-                intent.setAction(Intent.ACTION_VIEW);
+            if (id instanceof AbstractJingleConnection) {
                 intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.getSessionId());
-            } else if (id instanceof JingleConnectionManager.RtpSessionProposal) {
-                if (((JingleConnectionManager.RtpSessionProposal) id).media.contains(Media.VIDEO)) {
-                    intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
+                startActivity(intent);
+            } else if (id instanceof JingleConnectionManager.RtpSessionProposal proposal) {
+                if (Media.audioOnly(proposal.media)) {
+                    intent.putExtra(
+                            RtpSessionActivity.EXTRA_LAST_ACTION,
+                            RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
                 } else {
-                    intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
+                    intent.putExtra(
+                            RtpSessionActivity.EXTRA_LAST_ACTION,
+                            RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
                 }
+                intent.putExtra(RtpSessionActivity.EXTRA_PROPOSED_SESSION_ID, proposal.sessionId);
+                startActivity(intent);
             }
-            activity.startActivity(intent);
         }
     }
 
@@ -2124,7 +2120,7 @@ public class ConversationFragment extends XmppFragment
     }
 
     private void triggerRtpSession(final String action) {
-        if (activity.xmppConnectionService.getJingleConnectionManager().isBusy() != null) {
+        if (activity.xmppConnectionService.getJingleConnectionManager().isBusy()) {
             Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG)
                     .show();
             return;
@@ -2134,7 +2130,7 @@ public class ConversationFragment extends XmppFragment
             activity.xmppConnectionService.updateAccount(account);
         }
         final Contact contact = conversation.getContact();
-        if (RtpCapability.jmiSupport(contact)) {
+        if (Config.USE_JINGLE_MESSAGE_INIT && RtpCapability.jmiSupport(contact)) {
             triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
         } else {
             final RtpCapability.Capability capability;
@@ -2154,13 +2150,7 @@ public class ConversationFragment extends XmppFragment
     }
 
     private void triggerRtpSession(final Account account, final Jid with, final String action) {
-        final Intent intent = new Intent(activity, RtpSessionActivity.class);
-        intent.setAction(action);
-        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
-        intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        startActivity(intent);
+        CallIntegrationConnectionService.placeCall(activity.xmppConnectionService, account,with,RtpSessionActivity.actionToMedia(action));
     }
 
     private void handleAttachmentSelection(MenuItem item) {
@@ -2444,8 +2434,8 @@ public class ConversationFragment extends XmppFragment
 
     @SuppressLint("InflateParams")
     protected void clearHistoryDialog(final Conversation conversation) {
-        final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
-        builder.setTitle(getString(R.string.clear_conversation_history));
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
+        builder.setTitle(R.string.clear_conversation_history);
         final View dialogView =
                 requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
         final CheckBox endConversationCheckBox =
@@ -2468,7 +2458,7 @@ public class ConversationFragment extends XmppFragment
     }
 
     protected void muteConversationDialog(final Conversation conversation) {
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
         builder.setTitle(R.string.disable_notifications);
         final int[] durations = activity.getResources().getIntArray(R.array.mute_options_durations);
         final CharSequence[] labels = new CharSequence[durations.length];
@@ -2498,26 +2488,22 @@ public class ConversationFragment extends XmppFragment
     }
 
     private boolean hasPermissions(int requestCode, List<String> permissions) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            final List<String> missingPermissions = new ArrayList<>();
-            for (String permission : permissions) {
-                if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
-                    continue;
-                }
-                if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
-                    missingPermissions.add(permission);
-                }
+        final List<String> missingPermissions = new ArrayList<>();
+        for (String permission : permissions) {
+            if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+                continue;
             }
-            if (missingPermissions.size() == 0) {
-                return true;
-            } else {
-                requestPermissions(
-                        missingPermissions.toArray(new String[0]),
-                        requestCode);
-                return false;
+            if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+                missingPermissions.add(permission);
             }
-        } else {
+        }
+        if (missingPermissions.size() == 0) {
             return true;
+        } else {
+            requestPermissions(
+                    missingPermissions.toArray(new String[0]),
+                    requestCode);
+            return false;
         }
     }
 
@@ -2683,7 +2669,7 @@ public class ConversationFragment extends XmppFragment
     }
 
     private void showErrorMessage(final Message message) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
         builder.setTitle(R.string.error_message);
         final String errorMessage = message.getErrorMessage();
         final String[] errorMessageParts =
@@ -2754,7 +2740,7 @@ public class ConversationFragment extends XmppFragment
     }
 
     private void deleteFile(final Message message) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
         builder.setNegativeButton(R.string.cancel, null);
         builder.setTitle(R.string.delete_file_dialog);
         builder.setMessage(R.string.delete_file_dialog_msg);
@@ -2925,7 +2911,7 @@ public class ConversationFragment extends XmppFragment
     }
 
     @Override
-    public void onSaveInstanceState(@NotNull Bundle outState) {
+    public void onSaveInstanceState(@NonNull Bundle outState) {
         super.onSaveInstanceState(outState);
         if (conversation != null) {
             outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());
@@ -3401,10 +3387,10 @@ public class ConversationFragment extends XmppFragment
         final Iterator<Uri> iterator = uris.iterator();
         while (iterator.hasNext()) {
             final Uri uri = iterator.next();
-            if (FileBackend.weOwnFile(uri)) {
+            if (FileBackend.dangerousFile(uri)) {
                 iterator.remove();
                 Toast.makeText(
-                                getActivity(),
+                                requireActivity(),
                                 R.string.security_violation_not_attaching_file,
                                 Toast.LENGTH_SHORT)
                         .show();
@@ -3707,10 +3693,12 @@ public class ConversationFragment extends XmppFragment
             status = Presence.Status.OFFLINE;
         }
         this.binding.textSendButton.setTag(action);
+        this.binding.textSendButton.setIconTint(ColorStateList.valueOf(SendButtonTool.getSendButtonColor(this.binding.textSendButton, status)));
+        // TODO send button color
         final Activity activity = getActivity();
         if (activity != null) {
-            this.binding.textSendButton.setImageDrawable(
-                    SendButtonTool.getSendButtonImageResource(activity, action, status, text.length() > 0 || hasAttachments || (c.getThread() != null && binding.textinputSubject.getText().length() > 0)));
+            this.binding.textSendButton.setIconResource(
+                    SendButtonTool.getSendButtonImageResource(action, text.length() > 0 || hasAttachments || (c.getThread() != null && binding.textinputSubject.getText().length() > 0)));
         }
 
         ViewGroup.LayoutParams params = binding.threadIdenticonLayout.getLayoutParams();
@@ -4044,9 +4032,8 @@ public class ConversationFragment extends XmppFragment
                         });
     }
 
-    public void showNoPGPKeyDialog(boolean plural, DialogInterface.OnClickListener listener) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-        builder.setIconAttribute(android.R.attr.alertDialogIcon);
+    public void showNoPGPKeyDialog(final boolean plural, final DialogInterface.OnClickListener listener) {
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
         if (plural) {
             builder.setTitle(getString(R.string.no_pgp_keys));
             builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));

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

@@ -68,6 +68,7 @@ import com.cheogram.android.FinishOnboarding;
 import com.google.common.collect.ImmutableList;
 
 import io.michaelrocks.libphonenumber.android.NumberParseException;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 import org.openintents.openpgp.util.OpenPgpApi;
 
@@ -92,11 +93,11 @@ import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
 import eu.siacs.conversations.ui.interfaces.OnConversationRead;
 import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
 import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
-import eu.siacs.conversations.ui.util.ActionBarUtil;
 import eu.siacs.conversations.ui.util.ActivityResult;
 import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.ui.util.PendingItem;
+import eu.siacs.conversations.ui.util.ToolbarUtils;
 import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 import eu.siacs.conversations.utils.SignupUtils;
@@ -167,7 +168,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         if (performRedirectIfNecessary(true)) {
             return;
         }
@@ -254,10 +255,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     private boolean openBatteryOptimizationDialogIfNeeded() {
-        if (isOptimizingBattery()
-                && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M
-                && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
-            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        if (isOptimizingBattery() && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
             builder.setTitle(R.string.battery_optimizations_enabled);
             builder.setMessage(getString(R.string.battery_optimizations_enabled_dialog, getString(R.string.app_name)));
             builder.setPositiveButton(R.string.next, (dialog, which) -> {
@@ -508,6 +507,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         ConversationMenuConfigurator.reloadFeatures(this);
         OmemoSetting.load(this);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
         this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle);
@@ -602,9 +602,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras);
         if (mainNeedsRefresh) {
             refreshFragment(R.id.main_fragment);
-        } else {
-            invalidateActionBarTitle();
         }
+        invalidateActionBarTitle();
     }
 
     private static void executePendingTransactions(final FragmentManager fragmentManager) {
@@ -730,15 +729,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     @Override
-    protected void onStart() {
+    public void onStart() {
         super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme || !this.mCustomColors.equals(ThemeHelper.applyCustomColors(this))) {
-            this.mSkipBackgroundBinding = true;
-            recreate();
-        } else {
-            this.mSkipBackgroundBinding = false;
-        }
         mRedirectInProcess.set(false);
     }
 
@@ -814,21 +806,27 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         }
         final FragmentManager fragmentManager = getFragmentManager();
         final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
-        if (mainFragment instanceof ConversationFragment) {
-            final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
+        if (mainFragment instanceof ConversationFragment conversationFragment) {
+            final Conversation conversation = conversationFragment.getConversation();
             if (conversation != null) {
                 actionBar.setTitle(conversation.getName());
                 actionBar.setDisplayHomeAsUpEnabled(!xmppConnectionService.isOnboarding() || !conversation.getJid().equals(Jid.of("cheogram.com")));
-                ActionBarUtil.setActionBarOnClickListener(
-                        binding.toolbar,
-                        (v) -> { if(!xmppConnectionService.isOnboarding()) openConversationDetails(conversation); }
-                );
                 return;
             }
         }
-        actionBar.setTitle(R.string.app_name);
+        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
+        if (secondaryFragment instanceof ConversationFragment conversationFragment) {
+            final Conversation conversation = conversationFragment.getConversation();
+            if (conversation != null) {
+                actionBar.setTitle(conversation.getName());
+            } else {
+                actionBar.setTitle(R.string.app_name);
+            }
+        } else {
+            actionBar.setTitle(R.string.app_name);
+        }
         actionBar.setDisplayHomeAsUpEnabled(false);
-        ActionBarUtil.resetActionBarOnClickListeners(binding.toolbar);
+        ToolbarUtils.resetActionBarOnClickListeners(binding.toolbar);
     }
 
     private void openConversationDetails(final Conversation conversation) {

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

@@ -52,6 +52,8 @@ import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.snackbar.Snackbar;
 import com.google.common.base.Optional;
 import com.google.common.collect.Collections2;
@@ -76,7 +78,6 @@ import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.ui.util.PendingActionHelper;
 import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.ui.util.ScrollState;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.EasyOnboardingInvite;
 import eu.siacs.conversations.utils.ThemeHelper;
@@ -116,7 +117,7 @@ public class ConversationsOverviewFragment extends XmppFragment {
 			super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
 			if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
 				Paint paint = new Paint();
-				paint.setColor(StyledAttributes.getColor(activity,R.attr.conversations_overview_background));
+				paint.setColor(MaterialColors.getColor(viewHolder.itemView, com.google.android.material.R.attr.colorSecondaryFixedDim));
 				paint.setStyle(Paint.Style.FILL);
 				c.drawRect(viewHolder.itemView.getLeft(),viewHolder.itemView.getTop()
 						,viewHolder.itemView.getRight(),viewHolder.itemView.getBottom(), paint);
@@ -159,7 +160,7 @@ public class ConversationsOverviewFragment extends XmppFragment {
 					title = R.string.title_undo_swipe_out_channel;
 				}
 			} else {
-				title = R.string.title_undo_swipe_out_conversation;
+				title = R.string.title_undo_swipe_out_chat;
 			}
 
 			final Snackbar snackbar = Snackbar.make(binding.list, title, 5000)
@@ -201,8 +202,6 @@ public class ConversationsOverviewFragment extends XmppFragment {
 					activity.xmppConnectionService.archiveConversation(c);
 				}
 			});
-
-			ThemeHelper.fix(snackbar);
 			snackbar.show();
 		}
 	};
@@ -307,6 +306,9 @@ public class ConversationsOverviewFragment extends XmppFragment {
 		this.binding.list.setAdapter(this.conversationsAdapter);
 		this.binding.list.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false));
 		registerForContextMenu(this.binding.list);
+		this.binding.list.addOnScrollListener(ExtendedFabSizeChanger.of(binding.fab));
+		this.touchHelper = new ItemTouchHelper(this.callback);
+		this.touchHelper.attachToRecyclerView(this.binding.list);
 		return binding.getRoot();
 	}
 
@@ -456,14 +458,14 @@ public class ConversationsOverviewFragment extends XmppFragment {
 
 	private void selectAccountToStartEasyInvite() {
 		final List<Account> accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService);
-		if (accounts.size() == 0) {
+		if (accounts.isEmpty()) {
 			//This can technically happen if opening the menu item races with accounts reconnecting or something
 			Toast.makeText(getActivity(),R.string.no_active_accounts_support_this, Toast.LENGTH_LONG).show();
 		} else if (accounts.size() == 1) {
 			openEasyInviteScreen(accounts.get(0));
 		} else {
 			final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
-			final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
+			final MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(activity);
 			alertDialogBuilder.setTitle(R.string.choose_account);
 			final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toEscapedString()).toArray(new String[0]);
 			alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));

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

@@ -3,18 +3,20 @@ package eu.siacs.conversations.ui;
 import android.app.Dialog;
 import android.content.Context;
 import android.os.Bundle;
-import android.widget.Spinner;
+import android.widget.AutoCompleteTextView;
 
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 import androidx.fragment.app.DialogFragment;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 import java.util.ArrayList;
 import java.util.List;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.CreateConferenceDialogBinding;
+import eu.siacs.conversations.databinding.DialogCreateConferenceBinding;
 import eu.siacs.conversations.ui.util.DelayedHintHelper;
 
 public class CreatePrivateGroupChatDialog extends DialogFragment {
@@ -39,9 +41,9 @@ public class CreatePrivateGroupChatDialog extends DialogFragment {
     @NonNull
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
-        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
         builder.setTitle(R.string.create_private_group_chat);
-        CreateConferenceDialogBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.create_conference_dialog, null, false);
+        final DialogCreateConferenceBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_create_conference, null, false);
         ArrayList<String> mActivatedAccounts = getArguments().getStringArrayList(ACCOUNTS_LIST_KEY);
         StartConversationActivity.populateAccountSpinner(getActivity(), mActivatedAccounts, binding.account);
         builder.setView(binding.getRoot());
@@ -57,7 +59,7 @@ public class CreatePrivateGroupChatDialog extends DialogFragment {
 
 
     public interface CreateConferenceDialogListener {
-        void onCreateDialogPositiveClick(Spinner spinner, String subject);
+        void onCreateDialogPositiveClick(AutoCompleteTextView spinner, String subject);
     }
 
     @Override

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

@@ -17,13 +17,14 @@ import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 import androidx.fragment.app.DialogFragment;
 
-import java.security.SecureRandom;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.CreatePublicChannelDialogBinding;
+import eu.siacs.conversations.databinding.DialogCreatePublicChannelBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
@@ -44,7 +45,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
     private boolean nameEntered = false;
     private boolean skipTetxWatcher = false;
 
-    public static CreatePublicChannelDialog newInstance(List<String> accounts) {
+    public static CreatePublicChannelDialog newInstance(final List<String> accounts) {
         CreatePublicChannelDialog dialog = new CreatePublicChannelDialog();
         Bundle bundle = new Bundle();
         bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) accounts);
@@ -63,9 +64,9 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         jidWasModified = savedInstanceState != null && savedInstanceState.getBoolean("jid_was_modified_false", false);
         nameEntered = savedInstanceState != null && savedInstanceState.getBoolean("name_entered", false);
-        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
         builder.setTitle(R.string.create_public_channel);
-        final CreatePublicChannelDialogBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.create_public_channel_dialog, null, false);
+        final DialogCreatePublicChannelBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_create_public_channel, null, false);
         binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
             @Override
             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
@@ -107,7 +108,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
         builder.setPositiveButton(nameEntered ? R.string.create : R.string.next, null);
         builder.setNegativeButton(nameEntered ? R.string.back : R.string.cancel, null);
         DelayedHintHelper.setHint(R.string.channel_bare_jid_example, binding.jid);
-        this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item);
+        this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.item_autocomplete);
         binding.jid.setAdapter(knownHostsAdapter);
         final AlertDialog dialog = builder.create();
         binding.groupChatName.setOnEditorActionListener((v, actionId, event) -> {
@@ -121,7 +122,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
         return dialog;
     }
 
-    private void updateJidSuggestion(CreatePublicChannelDialogBinding binding) {
+    private void updateJidSuggestion(final DialogCreatePublicChannelBinding binding) {
         if (jidWasModified) {
             return;
         }
@@ -138,7 +139,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
         super.onSaveInstanceState(outState);
     }
 
-    private static String getJidSuggestion(CreatePublicChannelDialogBinding binding) {
+    private static String getJidSuggestion(final DialogCreatePublicChannelBinding binding) {
         final Account account = StartConversationActivity.getSelectedAccount(binding.getRoot().getContext(), binding.account);
         final XmppConnection connection = account == null ? null : account.getXmppConnection();
         if (connection == null) {
@@ -169,7 +170,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
         return name.replaceAll("\\s+","-");
     }
 
-    private void goBack(AlertDialog dialog, CreatePublicChannelDialogBinding binding) {
+    private void goBack(AlertDialog dialog, DialogCreatePublicChannelBinding binding) {
         if (nameEntered) {
             nameEntered = false;
             updateInputs(binding, true);
@@ -179,7 +180,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
         }
     }
 
-    private void submit(AlertDialog dialog, CreatePublicChannelDialogBinding binding) {
+    private void submit(AlertDialog dialog, DialogCreatePublicChannelBinding binding) {
         final Context context = binding.getRoot().getContext();
         final Editable nameText = binding.groupChatName.getText();
         final String name = nameText == null ? "" : nameText.toString().trim();
@@ -227,7 +228,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
     }
 
 
-    private void updateInputs(CreatePublicChannelDialogBinding binding, boolean requestFocus) {
+    private void updateInputs(final DialogCreatePublicChannelBinding binding, final boolean requestFocus) {
         binding.xmppAddressLayout.setVisibility(nameEntered ? View.VISIBLE : View.GONE);
         binding.nameLayout.setVisibility(nameEntered ? View.GONE : View.VISIBLE);
         if (!requestFocus) {
@@ -265,7 +266,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
     }
 
     @Override
-    public void onAttach(Context context) {
+    public void onAttach(@NonNull Context context) {
         super.onAttach(context);
         try {
             mListener = (CreatePublicChannelDialogListener) context;

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

@@ -36,9 +36,11 @@ import android.widget.Toast;
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AlertDialog.Builder;
 import androidx.databinding.DataBindingUtil;
+import androidx.lifecycle.Lifecycle;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
 import com.google.common.base.CharMatcher;
 
@@ -46,11 +48,15 @@ import com.rarepebble.colorpicker.ColorPickerView;
 
 import org.openintents.openpgp.util.OpenPgpUtils;
 
+import java.text.DateFormat;
 import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -66,6 +72,7 @@ import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
+import eu.siacs.conversations.ui.TimePreference;
 import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
 import eu.siacs.conversations.ui.adapter.PresenceTemplateAdapter;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
@@ -87,8 +94,16 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.XmppConnection.Features;
 import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.pep.Avatar;
+
 import okhttp3.HttpUrl;
 
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
 public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
         OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
 
@@ -104,7 +119,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     private Jid jidToEdit;
     private boolean mInitMode = false;
     private Boolean mForceRegister = null;
-    private boolean mUsernameMode = Config.DOMAIN_LOCK != null;
+    private boolean mUsernameMode = false;
     private boolean mShowOptions = false;
     private Account mAccount;
     private final OnClickListener mCancelButtonClickListener = v -> {
@@ -156,7 +171,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             final boolean accountInfoEdited = accountInfoEdited();
 
             ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground();
-            if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDarkTheme())) {
+            if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDark())) {
                 mAccount.setColor(previewColor.getColor());
             }
 
@@ -597,7 +612,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         return jidEdited() ||
                 !this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString()) ||
                 !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) ||
-                this.mAccount.getColor(isDarkTheme()) != (previewColor == null ? 0 : previewColor.getColor()) ||
+                this.mAccount.getColor(isDark()) != (previewColor == null ? 0 : previewColor.getColor()) ||
                 !String.valueOf(this.mAccount.getPort()).equals(this.binding.port.getText().toString());
     }
 
@@ -628,6 +643,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             this.mSavedInstanceInit = savedInstanceState.getBoolean("initMode", false);
         }
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_edit_account);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
         binding.accountJid.addTextChangedListener(this.mTextWatcher);
         binding.accountJid.setOnFocusChangeListener(this.mEditTextFocusListener);
@@ -653,11 +669,35 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         binding.accountColorBox.setOnClickListener((v) -> {
             showColorDialog();
         });
-        binding.quietHoursBox.setOnClickListener((v) -> {
-            Intent intent = new Intent(Intent.ACTION_VIEW, null, EditAccountActivity.this, SettingsActivity.class);
-            intent.putExtra("page", "quiet_hours");
-            intent.putExtra("suffix", ":" + mAccount.getUuid());
-            startActivity(intent);
+
+        final var preferences = getPreferences();
+        binding.quietHoursEnable.setOnClickListener((v) -> {
+            preferences.edit().putBoolean("enable_quiet_hours:" + mAccount.getUuid(), binding.quietHoursEnable.isChecked()).apply();
+            updateAccountInformation(false);
+        });
+        binding.quietHoursStartBox.setOnClickListener((v) -> {
+            final var picker = new com.google.android.material.timepicker.MaterialTimePicker.Builder()
+                .setTitleText("Quiet Hours Start")
+                .setHour((int)(preferences.getLong("quiet_hours_start:" + mAccount.getUuid(), 1320) / 60))
+                .setMinute((int)(preferences.getLong("quiet_hours_start:" + mAccount.getUuid(), 1320) % 60))
+                .build();
+            picker.addOnPositiveButtonClickListener((v2) -> {
+                preferences.edit().putLong("quiet_hours_start:" + mAccount.getUuid(), (picker.getHour() * 60) + picker.getMinute()).apply();
+                updateAccountInformation(false);
+            });
+            picker.show(getSupportFragmentManager(), "quiethoursstart");
+        });
+        binding.quietHoursEndBox.setOnClickListener((v) -> {
+            final var picker = new com.google.android.material.timepicker.MaterialTimePicker.Builder()
+                .setTitleText("Quiet Hours End")
+                .setHour((int)(preferences.getLong("quiet_hours_end:" + mAccount.getUuid(), 1320) / 60))
+                .setMinute((int)(preferences.getLong("quiet_hours_end:" + mAccount.getUuid(), 1320) % 60))
+                .build();
+            picker.addOnPositiveButtonClickListener((v2) -> {
+                preferences.edit().putLong("quiet_hours_end:" + mAccount.getUuid(), (picker.getHour() * 60) + picker.getMinute()).apply();
+                updateAccountInformation(false);
+            });
+            picker.show(getSupportFragmentManager(), "quiethoursend");
         });
         this.binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
     }
@@ -725,13 +765,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     @Override
-    protected void onStart() {
+    public void onStart() {
         super.onStart();
         final Intent intent = getIntent();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        } else if (intent != null) {
+        if (intent != null) {
             try {
                 this.jidToEdit = Jid.ofEscaped(intent.getStringExtra("jid"));
             } catch (final IllegalArgumentException | NullPointerException ignored) {
@@ -789,7 +826,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     private void displayVerificationWarningDialog(final XmppUri xmppUri) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(R.string.verify_omemo_keys);
         View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
         final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
@@ -804,7 +841,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             }
         });
         builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
-        AlertDialog dialog = builder.create();
+        final var dialog = builder.create();
         dialog.setCanceledOnTouchOutside(false);
         dialog.setOnCancelListener(d -> finish());
         dialog.show();
@@ -866,7 +903,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             this.binding.accountJidLayout.setHint(getString(R.string.username_hint));
         } else {
             final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
-                    R.layout.simple_list_item,
+                    R.layout.item_autocomplete,
                     xmppConnectionService.getKnownHosts());
             this.binding.accountJid.setAdapter(mKnownHostsAdapter);
         }
@@ -884,7 +921,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         if (mAccount != null && mAccount.getJid().getDomain() != null) {
             return mAccount.getServer();
         } else {
-            return Config.DOMAIN_LOCK;
+            return null;
         }
     }
 
@@ -983,8 +1020,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
 
     private void changePresence() {
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
-        boolean manualStatus = sharedPreferences.getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence));
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        boolean manualStatus = sharedPreferences.getBoolean(AppSettings.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence));
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         final DialogPresenceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
         String current = mAccount.getPresenceStatusMessage();
         if (current != null && !current.trim().isEmpty()) {
@@ -993,7 +1030,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding);
         binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE);
         List<PresenceTemplate> templates = xmppConnectionService.getPresenceTemplates(mAccount);
-        PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter(this, R.layout.simple_list_item, templates);
+        PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates);
         binding.statusMessage.setAdapter(presenceTemplateAdapter);
         binding.statusMessage.setOnItemClickListener((parent, view, position, id) -> {
             PresenceTemplate template = (PresenceTemplate) parent.getItemAtPosition(position);
@@ -1047,7 +1084,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         AlertDialog.Builder builder = new AlertDialog.Builder(this);
         final ColorPickerView picker = new ColorPickerView(this);
 
-        if (mAccount != null) picker.setColor(mAccount.getColor(isDarkTheme()));
+        if (mAccount != null) picker.setColor(mAccount.getColor(isDark()));
         picker.showAlpha(true);
         picker.showHex(true);
         picker.showPreview(true);
@@ -1081,6 +1118,27 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
 
         }
 
+        final var preferences = getPreferences();
+        binding.quietHoursEnable.setChecked(preferences.getBoolean("enable_quiet_hours:" + mAccount.getUuid(), false));
+
+        if (binding.quietHoursEnable.isChecked()) {
+            binding.quietHoursStartBox.setVisibility(View.VISIBLE);
+            binding.quietHoursEndBox.setVisibility(View.VISIBLE);
+        } else {
+            binding.quietHoursStartBox.setVisibility(View.GONE);
+            binding.quietHoursEndBox.setVisibility(View.GONE);
+        }
+
+        final var startTime = preferences.getLong("quiet_hours_start:" + mAccount.getUuid(), 1320);
+		final DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(this);
+		final Date date = TimePreference.minutesToCalender(startTime).getTime();
+        binding.quietHoursStart.setText(dateFormat.format(date.getTime()));
+
+        final var endTime = preferences.getLong("quiet_hours_end:" + mAccount.getUuid(), 480);
+		final DateFormat dateFormatE = android.text.format.DateFormat.getTimeFormat(this);
+		final Date dateE = TimePreference.minutesToCalender(endTime).getTime();
+        binding.quietHoursEnd.setText(dateFormat.format(dateE.getTime()));
+
         if (!mInitMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             this.binding.accountPassword.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
         }
@@ -1097,7 +1155,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
 
         if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() > 1) {
             binding.accountColorBox.setVisibility(View.VISIBLE);
-            binding.colorPreview.setBackgroundColor(mAccount.getColor(isDarkTheme()));
+            binding.colorPreview.setBackgroundColor(mAccount.getColor(isDark()));
             binding.quietHoursBox.setVisibility(View.VISIBLE);
         } else {
             binding.accountColorBox.setVisibility(View.GONE);
@@ -1215,7 +1273,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                 this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId));
                 this.binding.pgpFingerprint.setOnClickListener(openPgp);
                 if ("pgp".equals(messageFingerprint)) {
-                    this.binding.pgpFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
+                    this.binding.pgpFingerprintDesc.setTextColor(MaterialColors.getColor(binding.pgpFingerprintDesc, com.google.android.material.R.attr.colorPrimaryVariant));
                 }
                 this.binding.pgpFingerprintDesc.setOnClickListener(openPgp);
                 this.binding.actionDeletePgp.setOnClickListener(delete);
@@ -1226,10 +1284,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
                 this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
                 if (ownAxolotlFingerprint.equals(messageFingerprint)) {
-                    this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
+                    this.binding.ownFingerprintDesc.setTextColor(MaterialColors.getColor(binding.ownFingerprintDesc, com.google.android.material.R.attr.colorPrimaryVariant));
                     this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint_selected_message);
                 } else {
-                    this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption);
+                    this.binding.ownFingerprintDesc.setTextColor(MaterialColors.getColor(binding.ownFingerprintDesc, com.google.android.material.R.attr.colorOnSurface));
                     this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint);
                 }
                 this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2)));
@@ -1307,10 +1365,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     private void updateDisplayName(String displayName) {
         if (TextUtils.isEmpty(displayName)) {
             this.binding.yourName.setText(R.string.no_name_set_instructions);
-            this.binding.yourName.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1_Tertiary);
+            this.binding.yourName.setTextColor(MaterialColors.getColor(binding.yourName, com.google.android.material.R.attr.colorOnSurfaceVariant));
         } else {
             this.binding.yourName.setText(displayName);
-            this.binding.yourName.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1);
+            this.binding.yourName.setTextColor(MaterialColors.getColor(binding.yourName, com.google.android.material.R.attr.colorOnSurfaceVariant));
         }
     }
 
@@ -1334,7 +1392,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     private void showDeletePgpDialog() {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(R.string.unpublish_pgp);
         builder.setMessage(R.string.unpublish_pgp_message);
         builder.setNegativeButton(R.string.cancel, null);
@@ -1364,7 +1422,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                     Toast.makeText(EditAccountActivity.this, getString(R.string.device_does_not_support_data_saver, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
                 }
             });
-        } else if (showBatteryWarning && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+        } else if (showBatteryWarning) {
             this.binding.osOptimizationDisable.setText(R.string.disable);
             this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled);
             this.binding.osOptimizationBody.setText(getString(R.string.battery_optimizations_enabled_explained, getString(R.string.app_name)));
@@ -1382,7 +1440,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     public void showWipePepDialog() {
-        Builder builder = new Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(getString(R.string.clear_other_devices));
         builder.setIconAttribute(android.R.attr.alertDialogIcon);
         builder.setMessage(getString(R.string.clear_other_devices_desc));
@@ -1409,7 +1467,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
                 mCaptchaDialog.dismiss();
             }
-            final Builder builder = new Builder(EditAccountActivity.this);
+            if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
+                Log.d(Config.LOGTAG,"activity not running when captcha was requested");
+                return;
+            }
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(EditAccountActivity.this);
             final View view = getLayoutInflater().inflate(R.layout.captcha, null);
             final ImageView imageView = view.findViewById(R.id.captcha);
             final EditText input = view.findViewById(R.id.input);
@@ -1457,7 +1519,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             if (mFetchingMamPrefsToast != null) {
                 mFetchingMamPrefsToast.cancel();
             }
-            Builder builder = new Builder(EditAccountActivity.this);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(EditAccountActivity.this);
             builder.setTitle(R.string.server_side_mam_prefs);
             String defaultAttr = prefs.getAttribute("default");
             final List<String> defaults = Arrays.asList("never", "roster", "always");

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

@@ -1,4 +1,5 @@
 package eu.siacs.conversations.ui;
+import android.util.Log;
 
 import android.app.Activity;
 import android.app.Dialog;
@@ -24,6 +25,9 @@ import androidx.fragment.app.DialogFragment;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.LinearLayoutManager;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Strings;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -33,9 +37,8 @@ import java.util.Map;
 
 import io.michaelrocks.libphonenumber.android.NumberParseException;
 
-import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.EnterJidDialogBinding;
+import eu.siacs.conversations.databinding.DialogEnterJidBinding;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
@@ -68,7 +71,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
     private KnownHostsAdapter knownHostsAdapter;
     private Collection<String> whitelistedDomains = Collections.emptyList();
 
-    private EnterJidDialogBinding binding;
+    private DialogEnterJidBinding binding;
     private AlertDialog dialog;
     private SanityCheck sanityCheckJid = SanityCheck.NO;
 
@@ -82,7 +85,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
     }
 
     public static EnterJidDialog newInstance(
-            final List<String> activatedAccounts,
+            final ArrayList<String> activatedAccounts,
             final String title,
             final String positiveButton,
             final String secondaryButton,
@@ -91,7 +94,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
             boolean allowEditJid,
             boolean showBookmarkCheckbox,
             final SanityCheck sanity_check_jid) {
-        EnterJidDialog dialog = new EnterJidDialog();
+        final EnterJidDialog dialog = new EnterJidDialog();
         Bundle bundle = new Bundle();
         bundle.putString(TITLE_KEY, title);
         bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
@@ -99,7 +102,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
         bundle.putString(PREFILLED_JID_KEY, prefilledJid);
         bundle.putString(ACCOUNT_KEY, account);
         bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
-        bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) activatedAccounts);
+        bundle.putStringArrayList(ACCOUNTS_LIST_KEY, activatedAccounts);
         bundle.putInt(SANITY_CHECK_JID, sanity_check_jid.ordinal());
         bundle.putBoolean(SHOW_BOOKMARK_CHECKBOX, showBookmarkCheckbox);
         dialog.setArguments(bundle);
@@ -124,16 +127,16 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
 
     @NonNull
     @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-        builder.setTitle(getArguments().getString(TITLE_KEY));
+    public Dialog onCreateDialog(final Bundle savedInstanceState) {
+        final var arguments = getArguments();
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
+        builder.setTitle(arguments.getString(TITLE_KEY));
         binding =
-                DataBindingUtil.inflate(
-                        getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false);
-        this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item);
+                DataBindingUtil.inflate(requireActivity().getLayoutInflater(), R.layout.dialog_enter_jid, null, false);
+        this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.item_autocomplete);
         binding.jid.setAdapter(this.knownHostsAdapter);
         binding.jid.addTextChangedListener(this);
-        String prefilledJid = getArguments().getString(PREFILLED_JID_KEY);
+        final String prefilledJid = arguments.getString(PREFILLED_JID_KEY);
         if (prefilledJid != null) {
             binding.jid.append(prefilledJid);
             if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) {
@@ -151,18 +154,18 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
 
         DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
 
-        String account = getArguments().getString(ACCOUNT_KEY);
-        if (account == null) {
+        final String account = getArguments().getString(ACCOUNT_KEY);
+        if (Strings.isNullOrEmpty(account)) {
             StartConversationActivity.populateAccountSpinner(
                     getActivity(),
-                    getArguments().getStringArrayList(ACCOUNTS_LIST_KEY),
+                    arguments.getStringArrayList(ACCOUNTS_LIST_KEY),
                     binding.account);
         } else {
-            ArrayAdapter<String> adapter =
-                    new ArrayAdapter<>(
-                            getActivity(), R.layout.simple_list_item, new String[] {account});
+            final ArrayAdapter<String> adapter =
+                    new ArrayAdapter<>(requireActivity(), R.layout.item_autocomplete, new String[] {account});
+            binding.account.setText(account);
             binding.account.setEnabled(false);
-            adapter.setDropDownViewResource(R.layout.simple_list_item);
+            adapter.setDropDownViewResource(R.layout.item_autocomplete);
             binding.account.setAdapter(adapter);
         }
 
@@ -171,34 +174,13 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
         gatewayListAdapter.setOnEmpty(() -> binding.gatewayList.setVisibility(View.GONE));
         gatewayListAdapter.setOnNonEmpty(() -> binding.gatewayList.setVisibility(View.VISIBLE));
 
-        binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
-            @Override
-            public void onItemSelected(AdapterView accountSpinner, View view, int position, long id) {
-                XmppActivity context = (XmppActivity) getActivity();
-                if (context == null || context.xmppConnectionService == null || accountJid() == null) return;
-
-                gatewayListAdapter.clear();
-                final Account account = context.xmppConnectionService.findAccountByJid(accountJid());
-                if (account == null) return;
-
-                for (final Contact contact : account.getRoster().getContacts()) {
-                    if (contact.showInRoster() && contact.getPresences().size() > 0 && (contact.getPresences().anyIdentity("gateway", null) || contact.getPresences().anySupport("jabber:iq:gateway"))) {
-                        context.xmppConnectionService.fetchFromGateway(account, contact.getJid(), null, (final String prompt, String errorMessage) -> {
-                            if (prompt == null && !contact.getPresences().anyIdentity("gateway", null)) return;
-
-                            context.runOnUiThread(() -> {
-                                gatewayListAdapter.add(contact, prompt);
-                            });
-                        });
-                    }
-                }
-            }
-
+        binding.account.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
-            public void onNothingSelected(AdapterView accountSpinner) {
-                gatewayListAdapter.clear();
+            public void onItemClick(AdapterView accountSpinner, View view, int position, long id) {
+                populateGateways();
             }
         });
+        populateGateways();
 
         builder.setView(binding.getRoot());
         builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
@@ -224,19 +206,36 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
         return dialog;
     }
 
+    protected void populateGateways() {
+        XmppActivity context = (XmppActivity) getActivity();
+        if (context == null || context.xmppConnectionService == null || accountJid() == null) return;
+
+        gatewayListAdapter.clear();
+        final Account account = context.xmppConnectionService.findAccountByJid(accountJid());
+        if (account == null) return;
+
+        for (final Contact contact : account.getRoster().getContacts()) {
+            if (contact.showInRoster() && contact.getPresences().size() > 0 && (contact.getPresences().anyIdentity("gateway", null) || contact.getPresences().anySupport("jabber:iq:gateway"))) {
+                context.xmppConnectionService.fetchFromGateway(account, contact.getJid(), null, (final String prompt, String errorMessage) -> {
+                    if (prompt == null && !contact.getPresences().anyIdentity("gateway", null)) return;
+
+                    context.runOnUiThread(() -> {
+                            gatewayListAdapter.add(contact, prompt);
+                    });
+                });
+            }
+        }
+    }
+
     protected Jid accountJid() {
         try {
-            if (Config.DOMAIN_LOCK != null) {
-                return Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null);
-            } else {
-                return Jid.ofEscaped((String) binding.account.getSelectedItem());
-            }
+            return Jid.ofEscaped((String) binding.account.getEditableText().toString());
         } catch (final IllegalArgumentException e) {
             return null;
         }
     }
 
-    private void handleEnter(EnterJidDialogBinding binding, String account, boolean secondary) {
+    private void handleEnter(DialogEnterJidBinding binding, String account, boolean secondary) {
         if (!binding.account.isEnabled() && account == null) {
             return;
         }

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

@@ -0,0 +1,29 @@
+package eu.siacs.conversations.ui;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
+
+public class ExtendedFabSizeChanger extends RecyclerView.OnScrollListener {
+
+    private final ExtendedFloatingActionButton extendedFloatingActionButton;
+
+    private ExtendedFabSizeChanger(
+            final ExtendedFloatingActionButton extendedFloatingActionButton) {
+        this.extendedFloatingActionButton = extendedFloatingActionButton;
+    }
+
+    @Override
+    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+        super.onScrolled(recyclerView, dx, dy);
+        if (RecyclerViews.findFirstVisibleItemPosition(recyclerView) > 0) {
+            extendedFloatingActionButton.shrink();
+        } else {
+            extendedFloatingActionButton.extend();
+        }
+    }
+
+    public static RecyclerView.OnScrollListener of(final ExtendedFloatingActionButton fab) {
+        return new ExtendedFabSizeChanger(fab);
+    }
+}

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

@@ -13,6 +13,7 @@ import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 import androidx.fragment.app.DialogFragment;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
 
 import java.util.ArrayList;
@@ -52,11 +53,11 @@ public class JoinConferenceDialog extends DialogFragment implements OnBackendCon
 	@NonNull
 	@Override
 	public Dialog onCreateDialog(Bundle savedInstanceState) {
-		final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+		final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
 		builder.setTitle(R.string.join_public_channel);
-		DialogJoinConferenceBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_join_conference, null, false);
+		final DialogJoinConferenceBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_join_conference, null, false);
 		DelayedHintHelper.setHint(R.string.channel_full_jid_example, binding.jid);
-		this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item);
+		this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.item_autocomplete);
 		binding.jid.setAdapter(knownHostsAdapter);
 		String prefilledJid = getArguments().getString(PREFILLED_JID_KEY);
 		if (prefilledJid != null) {
@@ -119,6 +120,6 @@ public class JoinConferenceDialog extends DialogFragment implements OnBackendCon
 	}
 
 	public interface JoinConferenceDialogListener {
-		void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout jidLayout, AutoCompleteTextView jid, String password, boolean isBookmarkChecked);
+		void onJoinDialogPositiveClick(Dialog dialog, AutoCompleteTextView spinner, TextInputLayout jidLayout, AutoCompleteTextView jid, String password, boolean isBookmarkChecked);
 	}
 }

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

@@ -40,7 +40,6 @@ import eu.siacs.conversations.ui.util.LocationHelper;
 import eu.siacs.conversations.ui.widget.Marker;
 import eu.siacs.conversations.ui.widget.MyLocation;
 import eu.siacs.conversations.ui.util.SettingsUtils;
-import eu.siacs.conversations.utils.ThemeHelper;
 
 public abstract class LocationActivity extends ActionBarActivity implements LocationListener {
 	protected LocationManager locationManager;
@@ -78,7 +77,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
 	protected void onCreate(final Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		final Context ctx = getApplicationContext();
-		setTheme(ThemeHelper.find(this));
 
 		final PackageManager packageManager = ctx.getPackageManager();
 		hasLocationFeature = packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION) ||
@@ -90,7 +88,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
 		// Ask for location permissions if location services are enabled and we're
 		// just starting the activity (we don't want to keep pestering them on every
 		// screen rotation or if there's no point because it's disabled anyways).
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && savedInstanceState == null) {
+		if (savedInstanceState == null) {
 			requestPermissions(REQUEST_CODE_CREATE);
 		}
 
@@ -224,7 +222,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
 	@Override
 	protected void onResume() {
 		super.onResume();
-		SettingsUtils.applyScreenshotPreventionSetting(this);
 		Configuration.getInstance().load(this, getPreferences());
 		map.onResume();
 		this.setMyLoc(null);
@@ -239,13 +236,11 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
 		}
 	}
 
-	@TargetApi(Build.VERSION_CODES.M)
 	protected boolean hasLocationPermissions() {
 		return (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
 				checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED);
 	}
 
-	@TargetApi(Build.VERSION_CODES.M)
 	protected void requestPermissions(final int request_code) {
 		if (!hasLocationPermissions()) {
 			requestPermissions(

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

@@ -29,6 +29,7 @@ public class MediaBrowserActivity extends XmppActivity implements OnMediaLoaded
     protected void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         this.binding = DataBindingUtil.setContentView(this,R.layout.activity_media_browser);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
         mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
@@ -43,7 +44,7 @@ public class MediaBrowserActivity extends XmppActivity implements OnMediaLoaded
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         Intent intent = getIntent();
         String account = intent == null ? null : intent.getStringExtra("account");
         String jid = intent == null ? null : intent.getStringExtra("jid");

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

@@ -33,6 +33,8 @@ import android.os.Bundle;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatActivity;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -40,7 +42,6 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.MTMDecision;
 import eu.siacs.conversations.services.MemorizingTrustManager;
 import eu.siacs.conversations.ui.util.SettingsUtils;
-import eu.siacs.conversations.utils.ThemeHelper;
 
 public class MemorizingActivity extends AppCompatActivity implements OnClickListener, OnCancelListener {
 
@@ -53,23 +54,20 @@ public class MemorizingActivity extends AppCompatActivity implements OnClickList
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		LOGGER.log(Level.FINE, "onCreate");
-		setTheme(ThemeHelper.find(this));
 		super.onCreate(savedInstanceState);
-		getLayoutInflater().inflate(R.layout.toolbar, findViewById(android.R.id.content));
-		setSupportActionBar(findViewById(R.id.toolbar));
 	}
 
 	@Override
 	public void onResume() {
 		super.onResume();
-		SettingsUtils.applyScreenshotPreventionSetting(this);
+		SettingsUtils.applyScreenshotSetting(this);
 
 		Intent i = getIntent();
 		decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
 		int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert);
 		String cert = i.getStringExtra(MemorizingTrustManager.DECISION_INTENT_CERT);
 		LOGGER.log(Level.FINE, "onResume with " + i.getExtras() + " decId=" + decisionId + " data: " + i.getData());
-		dialog = new AlertDialog.Builder(this).setTitle(titleId)
+		dialog = new MaterialAlertDialogBuilder(this).setTitle(titleId)
 			.setMessage(cert)
 			.setPositiveButton(R.string.always, this)
 			.setNeutralButton(R.string.once, this)

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

@@ -49,7 +49,7 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         final Intent intent = getIntent();
         final String uuid = intent == null ? null : intent.getStringExtra("uuid");
         if (uuid != null) {
@@ -102,8 +102,9 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        ActivityMucUsersBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_users);
+        final ActivityMucUsersBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_users);
         setSupportActionBar(binding.toolbar);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         configureActionBar(getSupportActionBar(), true);
         this.userAdapter = new UserAdapter(getPreferences().getBoolean("advanced_muc_mode", false));
         binding.list.setAdapter(this.userAdapter);

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

@@ -11,6 +11,9 @@ import android.widget.Toast;
 import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -33,10 +36,7 @@ public abstract class OmemoActivity extends XmppActivity {
 		Object account = v.getTag(R.id.TAG_ACCOUNT);
 		Object fingerprint = v.getTag(R.id.TAG_FINGERPRINT);
 		Object fingerprintStatus = v.getTag(R.id.TAG_FINGERPRINT_STATUS);
-		if (account != null
-				&& fingerprint != null
-				&& account instanceof Account
-				&& fingerprintStatus != null
+		if (account instanceof Account
 				&& fingerprint instanceof String
 				&& fingerprintStatus instanceof FingerprintStatus) {
 			getMenuInflater().inflate(R.menu.omemo_key_context, menu);
@@ -130,8 +130,8 @@ public abstract class OmemoActivity extends XmppActivity {
 		binding.tglTrust.setChecked(status.isTrusted());
 
 		if (status.isActive()) {
-			binding.key.setTextAppearance(this,R.style.TextAppearance_Conversations_Fingerprint);
-			binding.keyType.setTextAppearance(this,R.style.TextAppearance_Conversations_Caption);
+			binding.key.setTextColor(MaterialColors.getColor(binding.key, com.google.android.material.R.attr.colorOnSurface));
+			binding.keyType.setTextColor(MaterialColors.getColor(binding.keyType, com.google.android.material.R.attr.colorOnSurface));
 			if (status.isVerified()) {
 				binding.verifiedFingerprint.setVisibility(View.VISIBLE);
 				binding.verifiedFingerprint.setAlpha(1.0f);
@@ -157,8 +157,8 @@ public abstract class OmemoActivity extends XmppActivity {
 				toast = v -> hideToast();
 			}
 		} else {
-			binding.key.setTextAppearance(this,R.style.TextAppearance_Conversations_Fingerprint_Disabled);
-			binding.keyType.setTextAppearance(this,R.style.TextAppearance_Conversations_Caption_Disabled);
+			binding.key.setTextColor(MaterialColors.getColor(binding.key, com.google.android.material.R.attr.colorOnSurfaceVariant));
+			binding.keyType.setTextColor(MaterialColors.getColor(binding.keyType, com.google.android.material.R.attr.colorOnSurfaceVariant));
 			toast = v -> replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
 			if (status.isVerified()) {
 				binding.tglTrust.setVisibility(View.GONE);
@@ -181,7 +181,7 @@ public abstract class OmemoActivity extends XmppActivity {
 			binding.keyType.setVisibility(View.GONE);
 		}
 		if (highlight) {
-			binding.keyType.setTextAppearance(this,R.style.TextAppearance_Conversations_Caption_Highlight);
+			binding.keyType.setTextColor(MaterialColors.getColor(binding.keyType, com.google.android.material.R.attr.colorPrimaryVariant));
 			binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
 		} else {
 			binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
@@ -191,7 +191,7 @@ public abstract class OmemoActivity extends XmppActivity {
 	}
 
 	public void showPurgeKeyDialog(final Account account, final String fingerprint) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 		builder.setTitle(R.string.distrust_omemo_key);
 		builder.setMessage(R.string.distrust_omemo_key_text);
 		builder.setNegativeButton(getString(R.string.cancel), null);

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

@@ -68,7 +68,7 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme
     }
 
     @Override
-    void onBackendConnected() {
+    protected void onBackendConnected() {
         String uuid = pendingConversationUuid.pop();
         if (uuid != null) {
             this.conversation = xmppConnectionService.findConversationByUuid(uuid);
@@ -99,6 +99,7 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(this.binding.toolbar);
         configureActionBar(getSupportActionBar());
         this.binding.cancelButton.setOnClickListener((v) -> this.finish());
@@ -122,6 +123,7 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme
 
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
         if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
             final CropImage.ActivityResult result = CropImage.getActivityResult(data);
             if (resultCode == RESULT_OK) {

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

@@ -21,6 +21,7 @@ import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
+import androidx.databinding.DataBindingUtil;
 
 import com.canhub.cropper.CropImage;
 
@@ -28,6 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
@@ -81,7 +83,6 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
     public void onAvatarPublicationFailed(int res) {
         runOnUiThread(() -> {
             hintOrWarning.setText(res);
-            hintOrWarning.setTextAppearance(this,R.style.TextAppearance_Conversations_Body1_Warning);
             hintOrWarning.setVisibility(View.VISIBLE);
             publishing = false;
             togglePublishButton(true, R.string.publish);
@@ -91,8 +92,12 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_publish_profile_picture);
-        setSupportActionBar(findViewById(R.id.toolbar));
+
+        ActivityPublishProfilePictureBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
+
+        setSupportActionBar(binding.toolbar);
+
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 
         this.avatar = findViewById(R.id.account_image);
         this.cancelButton = findViewById(R.id.cancel_button);
@@ -221,7 +226,7 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
     }
 
     @Override
-    protected void onStart() {
+    public void onStart() {
         super.onStart();
         final Intent intent = getIntent();
         this.mInitialAccountSetup = intent != null && intent.getBooleanExtra("setup", false);
@@ -270,7 +275,6 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
         if (bm == null) {
             togglePublishButton(false, R.string.publish);
             this.hintOrWarning.setVisibility(View.VISIBLE);
-            this.hintOrWarning.setTextAppearance(this,R.style.TextAppearance_Conversations_Body1_Warning);
             this.hintOrWarning.setText(R.string.error_publish_avatar_converting);
             return;
         }
@@ -284,7 +288,6 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
         } else {
             togglePublishButton(false, R.string.publish);
             this.hintOrWarning.setVisibility(View.VISIBLE);
-            this.hintOrWarning.setTextAppearance(this,R.style.TextAppearance_Conversations_Body1_Warning);
             if (account.getStatus() == Account.State.ONLINE) {
                 this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support);
             } else {

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

@@ -17,6 +17,7 @@ import android.view.View;
 import android.view.WindowManager;
 import android.widget.Toast;
 
+import androidx.appcompat.app.AppCompatActivity;
 import androidx.databinding.DataBindingUtil;
 
 import com.google.common.collect.ImmutableSet;
@@ -35,10 +36,9 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityRecordingBinding;
 import eu.siacs.conversations.ui.util.SettingsUtils;
-import eu.siacs.conversations.utils.ThemeHelper;
 import eu.siacs.conversations.utils.TimeFrameUtils;
 
-public class RecordingActivity extends Activity implements View.OnClickListener {
+public class RecordingActivity extends BaseActivity implements View.OnClickListener {
 
     private ActivityRecordingBinding binding;
 
@@ -63,7 +63,6 @@ public class RecordingActivity extends Activity implements View.OnClickListener
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        setTheme(ThemeHelper.findDialog(this));
         super.onCreate(savedInstanceState);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_recording);
         this.binding.cancelButton.setOnClickListener(this);
@@ -71,19 +70,13 @@ public class RecordingActivity extends Activity implements View.OnClickListener
         this.setFinishOnTouchOutside(false);
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
     }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        SettingsUtils.applyScreenshotPreventionSetting(this);
-    }
-
     @Override
-    protected void onStart() {
+    public void onStart() {
         super.onStart();
         if (!startRecording()) {
             this.binding.shareButton.setEnabled(false);
-            this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title);
+            this.binding.timer.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
+            // TODO reset font family. make red?
             this.binding.timer.setText(R.string.unable_to_start_recording);
         }
     }
@@ -112,6 +105,8 @@ public class RecordingActivity extends Activity implements View.OnClickListener
                     .add("ONEPLUS A6010")   // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133
                     .add("ONEPLUS A6013")   // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133
                     .add("Pixel 4a")        // Pixel 4a https://github.com/iNPUTmice/Conversations/issues/4223
+                    .add("WP12 Pro")        // Oukitel WP 12 Pro https://github.com/iNPUTmice/Conversations/issues/4223
+                    .add("Volla Phone X")   // Volla Phone X https://github.com/iNPUTmice/Conversations/issues/4223
                     .build();
 
     private boolean startRecording() {

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

@@ -0,0 +1,29 @@
+package eu.siacs.conversations.ui;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+public final class RecyclerViews {
+
+    private RecyclerViews() {
+        throw new IllegalStateException("Do not instantiate me");
+    }
+
+    public static boolean scrolledToTop(final RecyclerView recyclerView) {
+        final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+        if (layoutManager instanceof LinearLayoutManager linearLayoutManager) {
+            return linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0;
+        } else {
+            return false;
+        }
+    }
+
+    public static int findFirstVisibleItemPosition(final RecyclerView recyclerView) {
+        final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+        if (layoutManager instanceof LinearLayoutManager linearLayoutManager) {
+            return linearLayoutManager.findFirstVisibleItemPosition();
+        } else {
+            return RecyclerView.NO_POSITION;
+        }
+    }
+}

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

@@ -6,7 +6,6 @@ import static java.util.Arrays.asList;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
-import android.app.Activity;
 import android.app.PictureInPictureParams;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
@@ -18,7 +17,6 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.os.SystemClock;
 import android.util.Log;
 import android.util.Rational;
 import android.view.KeyEvent;
@@ -59,7 +57,8 @@ import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.services.AppRTCAudioManager;
+import eu.siacs.conversations.services.CallIntegration;
+import eu.siacs.conversations.services.CallIntegrationConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.widget.DialpadView;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
@@ -73,6 +72,7 @@ import eu.siacs.conversations.xmpp.jingle.ContentAddition;
 import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
+import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
 import eu.siacs.conversations.xmpp.jingle.RtpCapability;
 import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 
@@ -92,6 +92,7 @@ public class RtpSessionActivity extends XmppActivity
 
     public static final String EXTRA_WITH = "with";
     public static final String EXTRA_SESSION_ID = "session_id";
+    public static final String EXTRA_PROPOSED_SESSION_ID = "proposed_session_id";
     public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
     public static final String EXTRA_LAST_ACTION = "last_action";
     public static final String ACTION_ACCEPT_CALL = "action_accept_call";
@@ -105,6 +106,7 @@ public class RtpSessionActivity extends XmppActivity
                     RtpEndUserState.APPLICATION_ERROR,
                     RtpEndUserState.SECURITY_ERROR,
                     RtpEndUserState.DECLINED_OR_BUSY,
+                    RtpEndUserState.CONTACT_OFFLINE,
                     RtpEndUserState.CONNECTIVITY_ERROR,
                     RtpEndUserState.CONNECTIVITY_LOST_ERROR,
                     RtpEndUserState.RETRACTED);
@@ -126,6 +128,14 @@ public class RtpSessionActivity extends XmppActivity
                     RtpEndUserState.ACCEPTING_CALL,
                     RtpEndUserState.CONNECTING,
                     RtpEndUserState.RECONNECTING);
+    private static final List<RtpEndUserState> STATES_SHOWING_SPEAKER_CONFIGURATION =
+            new ImmutableList.Builder<RtpEndUserState>()
+                    .add(RtpEndUserState.FINDING_DEVICE)
+                    .add(RtpEndUserState.RINGING)
+                    .add(RtpEndUserState.ACCEPTING_CALL)
+                    .add(RtpEndUserState.CONNECTING)
+                    .addAll(STATES_CONSIDERED_CONNECTED)
+                    .build();
     private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
     private static final int REQUEST_ACCEPT_CALL = 0x1111;
     private static final int REQUEST_ACCEPT_CONTENT = 0x1112;
@@ -145,11 +155,16 @@ public class RtpSessionActivity extends XmppActivity
                 }
             };
 
-    private static Set<Media> actionToMedia(final String action) {
+    public static Set<Media> actionToMedia(final String action) {
         if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
             return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
-        } else {
+        } else if (ACTION_MAKE_VOICE_CALL.equals(action)) {
             return ImmutableSet.of(Media.AUDIO);
+        } else {
+            Log.w(
+                    Config.LOGTAG,
+                    "actionToMedia can not get media set from unknown action " + action);
+            return Collections.emptySet();
         }
     }
 
@@ -187,6 +202,8 @@ public class RtpSessionActivity extends XmppActivity
             boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
             binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
         }
+
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
     }
 
     @Override
@@ -315,14 +332,15 @@ public class RtpSessionActivity extends XmppActivity
     private void retractSessionProposal() {
         final Intent intent = getIntent();
         final String action = intent.getAction();
+        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
         final Account account = extractAccount(intent);
         final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
         final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
         if (!Intent.ACTION_VIEW.equals(action)
                 || state == null
                 || !END_CARD.contains(RtpEndUserState.valueOf(state))) {
-            resetIntent(
-                    account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction()));
+            final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
+            resetIntent(account, with, RtpEndUserState.RETRACTED, media);
         }
         xmppConnectionService
                 .getJingleConnectionManager()
@@ -390,7 +408,7 @@ public class RtpSessionActivity extends XmppActivity
         final List<String> permissions = permissions(getMedia());
         if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
             putScreenInCallMode();
-            checkRecorderAndAcceptCall();
+            acceptCall();
         }
     }
 
@@ -407,8 +425,7 @@ public class RtpSessionActivity extends XmppActivity
         return permissions.build();
     }
 
-    private void checkRecorderAndAcceptCall() {
-        checkMicrophoneAvailabilityAsync();
+    private void acceptCall() {
         try {
             requireRtpConnection().acceptCall();
         } catch (final IllegalStateException e) {
@@ -416,41 +433,6 @@ public class RtpSessionActivity extends XmppActivity
         }
     }
 
-    private void checkMicrophoneAvailabilityAsync() {
-        new Thread(new MicrophoneAvailabilityCheck(this)).start();
-    }
-
-    private static class MicrophoneAvailabilityCheck implements Runnable {
-
-        private final WeakReference<Activity> activityReference;
-
-        private MicrophoneAvailabilityCheck(final Activity activity) {
-            this.activityReference = new WeakReference<>(activity);
-        }
-
-        @Override
-        public void run() {
-            final long start = SystemClock.elapsedRealtime();
-            final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable();
-            final long stop = SystemClock.elapsedRealtime();
-            Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms");
-            if (isMicrophoneAvailable) {
-                return;
-            }
-            final Activity activity = activityReference.get();
-            if (activity == null) {
-                return;
-            }
-            activity.runOnUiThread(
-                    () ->
-                            Toast.makeText(
-                                            activity,
-                                            R.string.microphone_unavailable,
-                                            Toast.LENGTH_LONG)
-                                    .show());
-        }
-    }
-
     private void putScreenInCallMode() {
         putScreenInCallMode(requireRtpConnection().getMedia());
     }
@@ -460,11 +442,11 @@ public class RtpSessionActivity extends XmppActivity
         if (Media.audioOnly(media)) {
             final JingleRtpConnection rtpConnection =
                     rtpConnectionReference != null ? rtpConnectionReference.get() : null;
-            final AppRTCAudioManager audioManager =
-                    rtpConnection == null ? null : rtpConnection.getAudioManager();
-            if (audioManager == null
-                    || audioManager.getSelectedAudioDevice()
-                            == AppRTCAudioManager.AudioDevice.EARPIECE) {
+            final CallIntegration callIntegration =
+                    rtpConnection == null ? null : rtpConnection.getCallIntegration();
+            if (callIntegration == null
+                    || callIntegration.getSelectedAudioDevice()
+                            == CallIntegration.AudioDevice.EARPIECE) {
                 acquireProximityWakeLock();
             }
         }
@@ -509,9 +491,8 @@ public class RtpSessionActivity extends XmppActivity
         }
     }
 
-    private void putProximityWakeLockInProperState(
-            final AppRTCAudioManager.AudioDevice audioDevice) {
-        if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) {
+    private void putProximityWakeLockInProperState(final CallIntegration.AudioDevice audioDevice) {
+        if (audioDevice == CallIntegration.AudioDevice.EARPIECE) {
             acquireProximityWakeLock();
         } else {
             releaseProximityWakeLock();
@@ -525,6 +506,9 @@ public class RtpSessionActivity extends XmppActivity
     public void onNewIntent(final Intent intent) {
         Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()");
         super.onNewIntent(intent);
+        if (intent == null) {
+            return;
+        }
         setIntent(intent);
         if (xmppConnectionService == null) {
             Log.d(
@@ -532,32 +516,21 @@ public class RtpSessionActivity extends XmppActivity
                     "RtpSessionActivity: background service wasn't bound in onNewIntent()");
             return;
         }
-        final Account account = extractAccount(intent);
-        final String action = intent.getAction();
-        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
-        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
-        if (sessionId != null) {
-            Log.d(Config.LOGTAG, "reinitializing from onNewIntent()");
-            if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
-                return;
-            }
-            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
-                Log.d(Config.LOGTAG, "accepting call from onNewIntent()");
-                requestPermissionsAndAcceptCall();
-                resetIntent(intent.getExtras());
-            }
-        } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
-            proposeJingleRtpSession(account, with, actionToMedia(action));
-            setWith(account.getRoster().getContact(with), null);
-        } else {
-            throw new IllegalStateException("received onNewIntent without sessionId");
-        }
+        initializeWithIntent(Event.ON_NEW_INTENT, intent);
     }
 
     @Override
-    void onBackendConnected() {
-        final Intent intent = getIntent();
+    protected void onBackendConnected() {
+        final var intent = getIntent();
+        if (intent == null) {
+            return;
+        }
+        initializeWithIntent(Event.ON_BACKEND_CONNECTED, intent);
+    }
+
+    private void initializeWithIntent(final Event event, @NonNull final Intent intent) {
         final String action = intent.getAction();
+        Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")");
         final Account account = extractAccount(intent);
         final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
         final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
@@ -570,10 +543,17 @@ public class RtpSessionActivity extends XmppActivity
                 requestPermissionsAndAcceptCall();
                 resetIntent(intent.getExtras());
             }
-        } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
-            proposeJingleRtpSession(account, with, actionToMedia(action));
-            setWith(account.getRoster().getContact(with), null);
         } else if (Intent.ACTION_VIEW.equals(action)) {
+            final String proposedSessionId = intent.getStringExtra(EXTRA_PROPOSED_SESSION_ID);
+            final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
+                    xmppConnectionService
+                            .getJingleConnectionManager()
+                            .getTerminalSessionState(with, proposedSessionId);
+            if (terminatedRtpSession != null) {
+                // termination (due to message error or 'busy' was faster than opening the activity
+                initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
+                return;
+            }
             final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
             final RtpEndUserState state =
                     extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState);
@@ -591,10 +571,15 @@ public class RtpSessionActivity extends XmppActivity
                     .fireJingleRtpConnectionStateUpdates()) {
                 return;
             }
-            if (END_CARD.contains(state)
-                    || xmppConnectionService
-                            .getJingleConnectionManager()
-                            .hasMatchingProposal(account, with)) {
+            if (END_CARD.contains(state)) {
+                return;
+            }
+            final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
+            final Set<Media> media = actionToMedia(lastAction);
+            if (xmppConnectionService
+                    .getJingleConnectionManager()
+                    .hasMatchingProposal(account, with)) {
+                putScreenInCallMode(media);
                 return;
             }
             Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
@@ -617,24 +602,6 @@ public class RtpSessionActivity extends XmppActivity
         }
     }
 
-    private void proposeJingleRtpSession(
-            final Account account, final Jid with, final Set<Media> media) {
-        checkMicrophoneAvailabilityAsync();
-        if (with.isBareJid()) {
-            xmppConnectionService
-                    .getJingleConnectionManager()
-                    .proposeJingleRtpSession(account, with, media);
-        } else {
-            final String sessionId =
-                    xmppConnectionService
-                            .getJingleConnectionManager()
-                            .initializeRtpSession(account, with, media);
-            initializeActivityWithRunningRtpSession(account, with, sessionId);
-            resetIntent(account, with, sessionId);
-        }
-        putScreenInCallMode(media);
-    }
-
     @Override
     public void onRequestPermissionsResult(
             int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
@@ -643,7 +610,7 @@ public class RtpSessionActivity extends XmppActivity
                 PermissionUtils.removeBluetoothConnect(permissions, grantResults);
         if (PermissionUtils.allGranted(permissionResult.grantResults)) {
             if (requestCode == REQUEST_ACCEPT_CALL) {
-                checkRecorderAndAcceptCall();
+                acceptCall();
             } else if (requestCode == REQUEST_ACCEPT_CONTENT) {
                 acceptContentAdd();
             } else if (requestCode == REQUEST_ADD_CONTENT) {
@@ -924,6 +891,7 @@ public class RtpSessionActivity extends XmppActivity
             case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device);
             case RINGING -> setTitle(R.string.rtp_state_ringing);
             case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy);
+            case CONTACT_OFFLINE -> setTitle(R.string.rtp_state_contact_offline);
             case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error);
             case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error);
             case RETRACTED -> setTitle(R.string.rtp_state_retracted);
@@ -999,33 +967,34 @@ public class RtpSessionActivity extends XmppActivity
         } else if (state == RtpEndUserState.INCOMING_CALL) {
             this.binding.rejectCall.setContentDescription(getString(R.string.dismiss_call));
             this.binding.rejectCall.setOnClickListener(this::rejectCall);
-            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
+            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_24dp);
             this.binding.rejectCall.setVisibility(View.VISIBLE);
             this.binding.endCall.setVisibility(View.INVISIBLE);
             this.binding.acceptCall.setContentDescription(getString(R.string.answer_call));
             this.binding.acceptCall.setOnClickListener(this::acceptCall);
-            this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
+            this.binding.acceptCall.setImageResource(R.drawable.ic_call_24dp);
             this.binding.acceptCall.setVisibility(View.VISIBLE);
         } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
             this.binding.rejectCall.setContentDescription(
                     getString(R.string.reject_switch_to_video));
             this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
-            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
+            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
             this.binding.rejectCall.setVisibility(View.VISIBLE);
             this.binding.endCall.setVisibility(View.INVISIBLE);
             this.binding.acceptCall.setContentDescription(getString(R.string.accept));
             this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition)));
-            this.binding.acceptCall.setImageResource(R.drawable.ic_baseline_check_24);
+            this.binding.acceptCall.setImageResource(R.drawable.ic_check_24dp);
             this.binding.acceptCall.setVisibility(View.VISIBLE);
-        } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
+        } else if (asList(RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONTACT_OFFLINE)
+                .contains(state)) {
             this.binding.rejectCall.setContentDescription(getString(R.string.exit));
             this.binding.rejectCall.setOnClickListener(this::exit);
-            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
+            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
             this.binding.rejectCall.setVisibility(View.VISIBLE);
             this.binding.endCall.setVisibility(View.INVISIBLE);
             this.binding.acceptCall.setContentDescription(getString(R.string.record_voice_mail));
             this.binding.acceptCall.setOnClickListener(this::recordVoiceMail);
-            this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp);
+            this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_24dp);
             this.binding.acceptCall.setVisibility(View.VISIBLE);
         } else if (asList(
                         RtpEndUserState.CONNECTIVITY_ERROR,
@@ -1036,18 +1005,18 @@ public class RtpSessionActivity extends XmppActivity
                 .contains(state)) {
             this.binding.rejectCall.setContentDescription(getString(R.string.exit));
             this.binding.rejectCall.setOnClickListener(this::exit);
-            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
+            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
             this.binding.rejectCall.setVisibility(View.VISIBLE);
             this.binding.endCall.setVisibility(View.INVISIBLE);
             this.binding.acceptCall.setContentDescription(getString(R.string.try_again));
             this.binding.acceptCall.setOnClickListener(this::retry);
-            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
+            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_24dp);
             this.binding.acceptCall.setVisibility(View.VISIBLE);
         } else {
             this.binding.rejectCall.setVisibility(View.INVISIBLE);
             this.binding.endCall.setContentDescription(getString(R.string.hang_up));
             this.binding.endCall.setOnClickListener(this::endCall);
-            this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
+            this.binding.endCall.setImageResource(R.drawable.ic_call_end_24dp);
             this.binding.endCall.setVisibility(View.VISIBLE);
             this.binding.acceptCall.setVisibility(View.INVISIBLE);
         }
@@ -1071,16 +1040,16 @@ public class RtpSessionActivity extends XmppActivity
     private void updateInCallButtonConfiguration(
             final RtpEndUserState state, final Set<Media> media) {
         if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
-            Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
+            Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
             if (media.contains(Media.VIDEO)) {
                 final JingleRtpConnection rtpConnection = requireRtpConnection();
                 updateInCallButtonConfigurationVideo(
                         rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
             } else {
-                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
+                final CallIntegration callIntegration = requireRtpConnection().getCallIntegration();
                 updateInCallButtonConfigurationSpeaker(
-                        audioManager.getSelectedAudioDevice(),
-                        audioManager.getAudioDevices().size());
+                        callIntegration.getSelectedAudioDevice(),
+                        callIntegration.getAudioDevices().size());
                 this.binding.inCallActionFarRight.setVisibility(View.GONE);
             }
             if (media.contains(Media.AUDIO)) {
@@ -1089,6 +1058,20 @@ public class RtpSessionActivity extends XmppActivity
             } else {
                 this.binding.inCallActionLeft.setVisibility(View.GONE);
             }
+        } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state)
+                && !isPictureInPicture()
+                && Media.audioOnly(media)) {
+            final CallIntegration callIntegration;
+            try {
+                callIntegration = requireCallIntegration();
+            } catch (final IllegalStateException e) {
+                Log.e(Config.LOGTAG, "can not update InCallButtonConfiguration in state " + state);
+                return;
+            }
+            updateInCallButtonConfigurationSpeaker(
+                    callIntegration.getSelectedAudioDevice(),
+                    callIntegration.getAudioDevices().size());
+            this.binding.inCallActionFarRight.setVisibility(View.GONE);
         } else {
             this.binding.inCallActionLeft.setVisibility(View.GONE);
             this.binding.inCallActionRight.setVisibility(View.GONE);
@@ -1098,11 +1081,11 @@ public class RtpSessionActivity extends XmppActivity
 
     @SuppressLint("RestrictedApi")
     private void updateInCallButtonConfigurationSpeaker(
-            final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
+            final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
         switch (selectedAudioDevice) {
             case EARPIECE -> {
                 this.binding.inCallActionRight.setImageResource(
-                        R.drawable.ic_volume_off_black_24dp);
+                        R.drawable.ic_volume_off_24dp);
                 if (numberOfChoices >= 2) {
                     this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
                 } else {
@@ -1111,12 +1094,12 @@ public class RtpSessionActivity extends XmppActivity
                 }
             }
             case WIRED_HEADSET -> {
-                this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
+                this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_mic_24dp);
                 this.binding.inCallActionRight.setOnClickListener(null);
                 this.binding.inCallActionRight.setClickable(false);
             }
             case SPEAKER_PHONE -> {
-                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
+                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_24dp);
                 if (numberOfChoices >= 2) {
                     this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
                 } else {
@@ -1126,7 +1109,7 @@ public class RtpSessionActivity extends XmppActivity
             }
             case BLUETOOTH -> {
                 this.binding.inCallActionRight.setImageResource(
-                        R.drawable.ic_bluetooth_audio_black_24dp);
+                        R.drawable.ic_bluetooth_audio_24dp);
                 this.binding.inCallActionRight.setOnClickListener(null);
                 this.binding.inCallActionRight.setClickable(false);
             }
@@ -1140,17 +1123,17 @@ public class RtpSessionActivity extends XmppActivity
         this.binding.inCallActionRight.setVisibility(View.VISIBLE);
         if (isCameraSwitchable) {
             this.binding.inCallActionFarRight.setImageResource(
-                    R.drawable.ic_flip_camera_android_black_24dp);
+                    R.drawable.ic_flip_camera_android_24dp);
             this.binding.inCallActionFarRight.setVisibility(View.VISIBLE);
             this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
         } else {
             this.binding.inCallActionFarRight.setVisibility(View.GONE);
         }
         if (videoEnabled) {
-            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp);
+            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_24dp);
             this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
         } else {
-            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp);
+            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_24dp);
             this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
         }
     }
@@ -1204,10 +1187,10 @@ public class RtpSessionActivity extends XmppActivity
     @SuppressLint("RestrictedApi")
     private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
         if (microphoneEnabled) {
-            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp);
+            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_24dp);
             this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
         } else {
-            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp);
+            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_24dp);
             this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
         }
         this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
@@ -1337,21 +1320,17 @@ public class RtpSessionActivity extends XmppActivity
         }
     }
 
-    private void switchToEarpiece(View view) {
-        requireRtpConnection()
-                .getAudioManager()
-                .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
+    private void switchToEarpiece(final View view) {
+        requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
         acquireProximityWakeLock();
     }
 
-    private void switchToSpeaker(View view) {
-        requireRtpConnection()
-                .getAudioManager()
-                .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
+    private void switchToSpeaker(final View view) {
+        requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
         releaseProximityWakeLock();
     }
 
-    private void retry(View view) {
+    private void retry(final View view) {
         final Intent intent = getIntent();
         final Account account = extractAccount(intent);
         final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
@@ -1360,7 +1339,7 @@ public class RtpSessionActivity extends XmppActivity
         final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
         this.rtpConnectionReference = null;
         Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
-        proposeJingleRtpSession(account, with, media);
+        CallIntegrationConnectionService.placeCall(xmppConnectionService, account, with, media);
     }
 
     private void exit(final View view) {
@@ -1399,6 +1378,33 @@ public class RtpSessionActivity extends XmppActivity
         return connection;
     }
 
+    private CallIntegration requireCallIntegration() {
+        return requireOngoingRtpSession().getCallIntegration();
+    }
+
+    private OngoingRtpSession requireOngoingRtpSession() {
+        final JingleRtpConnection connection =
+                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
+        if (connection != null) {
+            return connection;
+        }
+        final Intent currentIntent = getIntent();
+        final String withExtra =
+                currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
+        final var account = extractAccount(currentIntent);
+        if (withExtra == null) {
+            throw new IllegalStateException("Current intent has no EXTRA_WITH");
+        }
+        final var matching =
+                xmppConnectionService
+                        .getJingleConnectionManager()
+                        .matchingProposal(account, Jid.of(withExtra));
+        if (matching.isPresent()) {
+            return matching.get();
+        }
+        throw new IllegalStateException("No matching session proposal");
+    }
+
     @Override
     public void onJingleRtpConnectionUpdate(
             Account account, Jid with, final String sessionId, RtpEndUserState state) {
@@ -1456,8 +1462,8 @@ public class RtpSessionActivity extends XmppActivity
 
     @Override
     public void onAudioDeviceChanged(
-            final AppRTCAudioManager.AudioDevice selectedAudioDevice,
-            final Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
+            final CallIntegration.AudioDevice selectedAudioDevice,
+            final Set<CallIntegration.AudioDevice> availableAudioDevices) {
         Log.d(
                 Config.LOGTAG,
                 "onAudioDeviceChanged in activity: selected:"
@@ -1465,19 +1471,26 @@ public class RtpSessionActivity extends XmppActivity
                         + ", available:"
                         + availableAudioDevices);
         try {
-            final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
-            final Set<Media> media = getMedia();
+            final OngoingRtpSession ongoingRtpSession = requireOngoingRtpSession();
+            final RtpEndUserState endUserState;
+            if (ongoingRtpSession instanceof JingleRtpConnection jingleRtpConnection) {
+                endUserState = jingleRtpConnection.getEndUserState();
+            } else {
+                // for session proposals all end user states are functionally the same
+                endUserState = RtpEndUserState.RINGING;
+            }
+            final Set<Media> media = ongoingRtpSession.getMedia();
             if (END_CARD.contains(endUserState)) {
                 Log.d(
                         Config.LOGTAG,
                         "onAudioDeviceChanged() nothing to do because end card has been reached");
             } else {
-                if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) {
-                    final AppRTCAudioManager audioManager =
-                            requireRtpConnection().getAudioManager();
+                if (Media.audioOnly(media)
+                        && STATES_SHOWING_SPEAKER_CONFIGURATION.contains(endUserState)) {
+                    final CallIntegration callIntegration = requireCallIntegration();
                     updateInCallButtonConfigurationSpeaker(
-                            audioManager.getSelectedAudioDevice(),
-                            audioManager.getAudioDevices().size());
+                            callIntegration.getSelectedAudioDevice(),
+                            callIntegration.getAudioDevices().size());
                 }
                 Log.d(
                         Config.LOGTAG,
@@ -1503,16 +1516,17 @@ public class RtpSessionActivity extends XmppActivity
         if (withExtra == null) {
             return;
         }
+        final Set<Media> media = actionToMedia(currentIntent.getStringExtra(EXTRA_LAST_ACTION));
         if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
             runOnUiThread(
                     () -> {
                         updateVerifiedShield(false);
                         updateStateDisplay(state);
-                        updateButtonConfiguration(state);
+                        updateButtonConfiguration(state, media, null);
                         updateIncomingCallScreen(state);
                         invalidateOptionsMenu();
                     });
-            resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
+            resetIntent(account, with, state, media);
         }
     }
 
@@ -1541,4 +1555,9 @@ public class RtpSessionActivity extends XmppActivity
     private static boolean emptyReference(final WeakReference<?> weakReference) {
         return weakReference == null || weakReference.get() == null;
     }
+
+    private enum Event {
+        ON_BACKEND_CONNECTED,
+        ON_NEW_INTENT
+    }
 }

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

@@ -42,8 +42,10 @@ import android.view.View;
 import android.widget.AdapterView;
 import android.widget.EditText;
 
+import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.color.MaterialColors;
 import com.google.common.base.Strings;
 
 import java.lang.ref.WeakReference;
@@ -64,7 +66,6 @@ import eu.siacs.conversations.ui.util.DateSeparator;
 import eu.siacs.conversations.ui.util.ListViewUtils;
 import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.ui.util.ShareUtil;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.utils.FtsUtils;
 import eu.siacs.conversations.utils.MessageUtils;
 
@@ -95,6 +96,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
 		}
 		super.onCreate(bundle);
 		this.binding = DataBindingUtil.setContentView(this, R.layout.activity_search);
+		Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 		setSupportActionBar(this.binding.toolbar);
 		configureActionBar(getSupportActionBar());
 		this.messageListAdapter = new MessageAdapter(this, this.messages, uuid == null);
@@ -213,7 +215,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
 	}
 
 	@Override
-	void onBackendConnected() {
+    protected void onBackendConnected() {
 		final List<String> searchTerm = pendingSearch.pop();
 		if (searchTerm != null && currentSearch.watch(searchTerm)) {
 			xmppConnectionService.search(searchTerm, uuid,this);
@@ -223,12 +225,12 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
 	private void changeBackground(boolean hasSearch, boolean hasResults) {
 		if (hasSearch) {
 			if (hasResults) {
-				binding.searchResults.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_secondary));
+				binding.searchResults.setBackgroundColor(MaterialColors.getColor(binding.searchResults, com.google.android.material.R.attr.colorSurface));
 			} else {
-				binding.searchResults.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_background_no_results));
+				binding.searchResults.setBackgroundResource(R.drawable.background_no_results);
 			}
 		} else {
-			binding.searchResults.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_background_search));
+			binding.searchResults.setBackgroundResource(R.drawable.background_search);
 		}
 	}
 
@@ -248,14 +250,14 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
 		if (!currentSearch.watch(term)) {
 			return;
 		}
-		if (term.size() > 0) {
-			xmppConnectionService.search(term, uuid,this);
-		} else {
+		if (term.isEmpty()) {
 			MessageSearchTask.cancelRunningTasks();
 			this.messages.clear();
 			messageListAdapter.setHighlightedTerm(null);
 			messageListAdapter.notifyDataSetChanged();
 			changeBackground(false, false);
+		} else {
+			xmppConnectionService.search(term, uuid,this);
 		}
 	}
 
@@ -267,7 +269,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
 			DateSeparator.addAll(messages);
 			this.messages.addAll(messages);
 			messageListAdapter.notifyDataSetChanged();
-			changeBackground(true, messages.size() > 0);
+			changeBackground(true, !messages.isEmpty());
 			ListViewUtils.scrollToBottom(this.binding.searchResults);
 		});
 	}

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

@@ -1,730 +0,0 @@
-package eu.siacs.conversations.ui;
-
-import android.app.FragmentManager;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.storage.StorageManager;
-import android.preference.CheckBoxPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceManager;
-import android.preference.PreferenceScreen;
-import android.provider.MediaStore;
-import android.util.Log;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.core.content.ContextCompat;
-
-import com.cheogram.android.DownloadDefaultStickers;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-
-import java.io.File;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.KeyStoreException;
-import java.util.ArrayList;
-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.crypto.OmemoSetting;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.services.ExportBackupService;
-import eu.siacs.conversations.services.MemorizingTrustManager;
-import eu.siacs.conversations.services.QuickConversationsService;
-import eu.siacs.conversations.services.UnifiedPushDistributor;
-import eu.siacs.conversations.ui.util.SettingsUtils;
-import eu.siacs.conversations.ui.util.StyledAttributes;
-import eu.siacs.conversations.utils.GeoHelper;
-import eu.siacs.conversations.utils.ThemeHelper;
-import eu.siacs.conversations.utils.TimeFrameUtils;
-import eu.siacs.conversations.xmpp.InvalidJid;
-import eu.siacs.conversations.xmpp.Jid;
-
-public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener {
-
-    public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
-    public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off";
-    public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent";
-    public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode";
-    public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence";
-    public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
-    public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
-    public static final String BROADCAST_LAST_ACTIVITY = "last_activity";
-    public static final String THEME = "theme";
-    public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
-    public static final String OMEMO_SETTING = "omemo";
-    public static final String PREVENT_SCREENSHOTS = "prevent_screenshots";
-
-    public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
-    public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702;
-
-    private SettingsFragment mSettingsFragment;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_settings);
-        FragmentManager fm = getFragmentManager();
-        mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content);
-        if (mSettingsFragment == null
-                || !mSettingsFragment.getClass().equals(SettingsFragment.class)) {
-            mSettingsFragment = new SettingsFragment();
-            fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit();
-        }
-        mSettingsFragment.setActivityIntent(getIntent());
-        this.mTheme = findTheme();
-        setTheme(this.mTheme);
-        ThemeHelper.applyCustomColors(this);
-        getWindow()
-                .getDecorView()
-                .setBackgroundColor(
-                        StyledAttributes.getColor(this, R.attr.color_background_primary));
-        setSupportActionBar(findViewById(R.id.toolbar));
-        configureActionBar(getSupportActionBar());
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (data == null || data.getData() == null) return;
-
-        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this);
-        p.edit().putString("sticker_directory", data.getData().toString()).commit();
-    }
-
-    @Override
-    void onBackendConnected() {
-        boolean diallerIntegrationPossible = false;
-
-        if (Build.VERSION.SDK_INT >= 23) {
-            outer:
-            for (Account account : xmppConnectionService.getAccounts()) {
-                for (Contact contact : account.getRoster().getContacts()) {
-                    if (contact.getPresences().anyIdentity("gateway", "pstn")) {
-                        diallerIntegrationPossible = true;
-                        break outer;
-                    }
-                }
-            }
-        }
-        if (!diallerIntegrationPossible) {
-            PreferenceCategory cat = (PreferenceCategory) mSettingsFragment.findPreference("notification_category");
-            Preference pref = mSettingsFragment.findPreference("dialler_integration_incoming");
-            if (cat != null && pref != null) cat.removePreference(pref);
-        }
-        if (xmppConnectionService.getAccounts().size() > 1) {
-            PreferenceCategory cat = (PreferenceCategory) mSettingsFragment.findPreference("notification_category");
-            Preference pref = mSettingsFragment.findPreference("quiet_hours");
-            if (cat != null && pref != null) cat.removePreference(pref);
-        }
-        final Preference accountPreference =
-                mSettingsFragment.findPreference(UnifiedPushDistributor.PREFERENCE_ACCOUNT);
-        reconfigureUpAccountPreference(accountPreference);
-    }
-
-    private void reconfigureUpAccountPreference(final Preference preference) {
-        final ListPreference listPreference;
-        if (preference instanceof ListPreference) {
-            listPreference = (ListPreference) preference;
-        } else {
-            return;
-        }
-        final List<CharSequence> accounts =
-                ImmutableList.copyOf(
-                        Lists.transform(
-                                xmppConnectionService.getAccounts(),
-                                a -> a.getJid().asBareJid().toEscapedString()));
-        final ImmutableList.Builder<CharSequence> entries = new ImmutableList.Builder<>();
-        final ImmutableList.Builder<CharSequence> entryValues = new ImmutableList.Builder<>();
-        entries.add(getString(R.string.no_account_deactivated));
-        entryValues.add("none");
-        entries.addAll(accounts);
-        entryValues.addAll(accounts);
-        listPreference.setEntries(entries.build().toArray(new CharSequence[0]));
-        listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0]));
-        if (!accounts.contains(listPreference.getValue())) {
-            listPreference.setValue("none");
-        }
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        PreferenceManager.getDefaultSharedPreferences(this)
-                .registerOnSharedPreferenceChangeListener(this);
-
-        changeOmemoSettingSummary();
-
-        if (QuickConversationsService.isQuicksy()
-                || QuickConversationsService.isPlayStoreFlavor()
-                || Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
-            final PreferenceCategory groupChats =
-                    (PreferenceCategory) mSettingsFragment.findPreference("group_chats");
-            final Preference channelDiscoveryMethod =
-                    mSettingsFragment.findPreference("channel_discovery_method");
-            if (groupChats != null && channelDiscoveryMethod != null) {
-                groupChats.removePreference(channelDiscoveryMethod);
-            }
-        }
-
-        if (QuickConversationsService.isQuicksy()) {
-            final PreferenceCategory connectionOptions =
-                    (PreferenceCategory) mSettingsFragment.findPreference("connection_options");
-            PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert");
-            if (connectionOptions != null) {
-                expert.removePreference(connectionOptions);
-            }
-        }
-
-        PreferenceScreen mainPreferenceScreen =
-                (PreferenceScreen) mSettingsFragment.findPreference("main_screen");
-
-        PreferenceCategory attachmentsCategory =
-                (PreferenceCategory) mSettingsFragment.findPreference("attachments");
-        CheckBoxPreference locationPlugin =
-                (CheckBoxPreference) mSettingsFragment.findPreference("use_share_location_plugin");
-        if (attachmentsCategory != null && locationPlugin != null) {
-            if (!GeoHelper.isLocationPluginInstalled(this)) {
-                attachmentsCategory.removePreference(locationPlugin);
-            }
-        }
-
-        // this feature is only available on Huawei Android 6.
-        PreferenceScreen huaweiPreferenceScreen =
-                (PreferenceScreen) mSettingsFragment.findPreference("huawei");
-        if (huaweiPreferenceScreen != null) {
-            Intent intent = huaweiPreferenceScreen.getIntent();
-            // remove when Api version is above M (Version 6.0) or if the intent is not callable
-            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M || !isCallable(intent)) {
-                PreferenceCategory generalCategory =
-                        (PreferenceCategory) mSettingsFragment.findPreference("general");
-                generalCategory.removePreference(huaweiPreferenceScreen);
-                if (generalCategory.getPreferenceCount() == 0) {
-                    if (mainPreferenceScreen != null) {
-                        mainPreferenceScreen.removePreference(generalCategory);
-                    }
-                }
-            }
-        }
-
-        ListPreference automaticMessageDeletionList =
-                (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_MESSAGE_DELETION);
-        if (automaticMessageDeletionList != null) {
-            final int[] choices =
-                    getResources().getIntArray(R.array.automatic_message_deletion_values);
-            CharSequence[] entries = new CharSequence[choices.length];
-            CharSequence[] entryValues = new CharSequence[choices.length];
-            for (int i = 0; i < choices.length; ++i) {
-                entryValues[i] = String.valueOf(choices[i]);
-                if (choices[i] == 0) {
-                    entries[i] = getString(R.string.never);
-                } else {
-                    entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]);
-                }
-            }
-            automaticMessageDeletionList.setEntries(entries);
-            automaticMessageDeletionList.setEntryValues(entryValues);
-        }
-
-        boolean removeLocation =
-                new Intent("eu.siacs.conversations.location.request")
-                                .resolveActivity(getPackageManager())
-                        == null;
-        boolean removeVoice =
-                new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
-                                .resolveActivity(getPackageManager())
-                        == null;
-
-        ListPreference quickAction =
-                (ListPreference) mSettingsFragment.findPreference("quick_action");
-        if (quickAction != null && (removeLocation || removeVoice)) {
-            ArrayList<CharSequence> entries =
-                    new ArrayList<>(Arrays.asList(quickAction.getEntries()));
-            ArrayList<CharSequence> entryValues =
-                    new ArrayList<>(Arrays.asList(quickAction.getEntryValues()));
-            int index = entryValues.indexOf("location");
-            if (index > 0 && removeLocation) {
-                entries.remove(index);
-                entryValues.remove(index);
-            }
-            index = entryValues.indexOf("voice");
-            if (index > 0 && removeVoice) {
-                entries.remove(index);
-                entryValues.remove(index);
-            }
-            quickAction.setEntries(entries.toArray(new CharSequence[entries.size()]));
-            quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()]));
-        }
-
-        final Preference removeCertsPreference =
-                mSettingsFragment.findPreference("remove_trusted_certificates");
-        if (removeCertsPreference != null) {
-            removeCertsPreference.setOnPreferenceClickListener(
-                    preference -> {
-                        final MemorizingTrustManager mtm =
-                                xmppConnectionService.getMemorizingTrustManager();
-                        final ArrayList<String> aliases = Collections.list(mtm.getCertificates());
-                        if (aliases.size() == 0) {
-                            displayToast(getString(R.string.toast_no_trusted_certs));
-                            return true;
-                        }
-                        final ArrayList<Integer> selectedItems = new ArrayList<>();
-                        final AlertDialog.Builder dialogBuilder =
-                                new AlertDialog.Builder(SettingsActivity.this);
-                        dialogBuilder.setTitle(
-                                getResources().getString(R.string.dialog_manage_certs_title));
-                        dialogBuilder.setMultiChoiceItems(
-                                aliases.toArray(new CharSequence[aliases.size()]),
-                                null,
-                                (dialog, indexSelected, isChecked) -> {
-                                    if (isChecked) {
-                                        selectedItems.add(indexSelected);
-                                    } else if (selectedItems.contains(indexSelected)) {
-                                        selectedItems.remove(Integer.valueOf(indexSelected));
-                                    }
-                                    ((AlertDialog) dialog)
-                                            .getButton(DialogInterface.BUTTON_POSITIVE)
-                                            .setEnabled(selectedItems.size() > 0);
-                                });
-
-                        dialogBuilder.setPositiveButton(
-                                getResources()
-                                        .getString(R.string.dialog_manage_certs_positivebutton),
-                                (dialog, which) -> {
-                                    int count = selectedItems.size();
-                                    if (count > 0) {
-                                        for (int i = 0; i < count; i++) {
-                                            try {
-                                                Integer item =
-                                                        Integer.valueOf(
-                                                                selectedItems.get(i).toString());
-                                                String alias = aliases.get(item);
-                                                mtm.deleteCertificate(alias);
-                                            } catch (KeyStoreException e) {
-                                                e.printStackTrace();
-                                                displayToast("Error: " + e.getLocalizedMessage());
-                                            }
-                                        }
-                                        if (xmppConnectionServiceBound) {
-                                            reconnectAccounts();
-                                        }
-                                        displayToast(
-                                                getResources()
-                                                        .getQuantityString(
-                                                                R.plurals.toast_delete_certificates,
-                                                                count,
-                                                                count));
-                                    }
-                                });
-                        dialogBuilder.setNegativeButton(
-                                getResources()
-                                        .getString(R.string.dialog_manage_certs_negativebutton),
-                                null);
-                        AlertDialog removeCertsDialog = dialogBuilder.create();
-                        removeCertsDialog.show();
-                        removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
-                        return true;
-                    });
-        }
-
-        final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup");
-        if (createBackupPreference != null) {
-            createBackupPreference.setSummary(
-                    getString(
-                            R.string.pref_create_backup_summary,
-                            FileBackend.getBackupDirectory(this).getAbsolutePath()));
-            createBackupPreference.setOnPreferenceClickListener(
-                    preference -> {
-                        if (hasStoragePermission(REQUEST_CREATE_BACKUP)) {
-                            createBackup();
-                        }
-                        return true;
-                    });
-        }
-
-        if (Config.ONLY_INTERNAL_STORAGE) {
-            final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache");
-            if (cleanCachePreference != null) {
-                cleanCachePreference.setOnPreferenceClickListener(preference -> cleanCache());
-            }
-
-            final Preference cleanPrivateStoragePreference =
-                    mSettingsFragment.findPreference("clean_private_storage");
-            if (cleanPrivateStoragePreference != null) {
-                cleanPrivateStoragePreference.setOnPreferenceClickListener(
-                        preference -> cleanPrivateStorage());
-            }
-        }
-
-        final Preference deleteOmemoPreference =
-                mSettingsFragment.findPreference("delete_omemo_identities");
-        if (deleteOmemoPreference != null) {
-            deleteOmemoPreference.setOnPreferenceClickListener(
-                    preference -> deleteOmemoIdentities());
-        }
-        if (Config.omemoOnly()) {
-            final PreferenceCategory privacyCategory =
-                    (PreferenceCategory) mSettingsFragment.findPreference("privacy");
-            final Preference omemoPreference =mSettingsFragment.findPreference(OMEMO_SETTING);
-            if (omemoPreference != null) {
-                privacyCategory.removePreference(omemoPreference);
-            }
-        }
-
-        final Preference stickerDir = mSettingsFragment.findPreference("sticker_directory");
-        if (stickerDir != null) {
-            if (Build.VERSION.SDK_INT >= 24) {
-                stickerDir.setOnPreferenceClickListener((p) -> {
-                    Intent intent = ((StorageManager) getSystemService(Context.STORAGE_SERVICE)).getPrimaryStorageVolume().createOpenDocumentTreeIntent();
-                    startActivityForResult(Intent.createChooser(intent, "Choose sticker location"), 0);
-                    return true;
-                });
-            } else {
-                PreferenceCategory expertMedia = (PreferenceCategory) mSettingsFragment.findPreference("expert_media");
-                expertMedia.removePreference(stickerDir);
-            }
-        }
-
-        final Preference downloadDefaultStickers = mSettingsFragment.findPreference("download_default_stickers");
-        if (downloadDefaultStickers != null) {
-            downloadDefaultStickers.setOnPreferenceClickListener(
-                    preference -> {
-                        if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) {
-                            downloadStickers();
-                        }
-                        return true;
-                    });
-        }
-
-        final Preference clearBlockedMedia = mSettingsFragment.findPreference("clear_blocked_media");
-        if (clearBlockedMedia != null) {
-            clearBlockedMedia.setOnPreferenceClickListener((p) -> {
-                xmppConnectionService.clearBlockedMedia();
-                displayToast("Blocked media will be displayed again.");
-                return true;
-            });
-        }
-
-        final String theTheme = PreferenceManager.getDefaultSharedPreferences(this).getString(THEME, "");
-        if (Build.VERSION.SDK_INT < 30 || !theTheme.equals("custom")) {
-            final PreferenceCategory uiCategory = (PreferenceCategory) mSettingsFragment.findPreference("ui");
-            final Preference customTheme = mSettingsFragment.findPreference("custom_theme");
-            if (customTheme != null) uiCategory.removePreference(customTheme);
-        }
-
-        if (Build.VERSION.SDK_INT > 30 && theTheme.equals("custom")) {
-            final boolean customAutomatic = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("custom_theme_automatic", false);
-            final PreferenceScreen customTheme = (PreferenceScreen) mSettingsFragment.findPreference("custom_theme");
-            final Preference customThemeIsDark = mSettingsFragment.findPreference("custom_theme_dark");
-
-            if (customAutomatic) {
-                if (customTheme != null && customThemeIsDark != null) customTheme.removePreference(customThemeIsDark);
-            } else {
-                final boolean isDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("custom_theme_dark", false);
-                if (isDark) {
-                    final PreferenceCategory customThemeColors = (PreferenceCategory) mSettingsFragment.findPreference("custom_theme_colors");
-                    if (customTheme != null && customThemeColors != null) customTheme.removePreference(customThemeColors);
-                } else {
-                    final PreferenceCategory customThemeColorsDark = (PreferenceCategory) mSettingsFragment.findPreference("custom_theme_colors_dark");
-                    if (customTheme != null && customThemeColorsDark != null) customTheme.removePreference(customThemeColorsDark);
-                }
-            }
-        }
-    }
-
-    private void changeOmemoSettingSummary() {
-        final ListPreference omemoPreference =
-                (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING);
-        if (omemoPreference == null) {
-            return;
-        }
-        final String value = omemoPreference.getValue();
-        switch (value) {
-            case "always":
-                omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always);
-                break;
-            case "default_on":
-                omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on);
-                break;
-            case "default_off":
-                omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off);
-                break;
-        }
-    }
-
-    private boolean isCallable(final Intent i) {
-        return i != null
-                && getPackageManager()
-                                .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY)
-                                .size()
-                        > 0;
-    }
-
-    private boolean cleanCache() {
-        Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
-        intent.setData(Uri.parse("package:" + getPackageName()));
-        startActivity(intent);
-        return true;
-    }
-
-    private boolean cleanPrivateStorage() {
-        for (String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) {
-            cleanPrivateFiles(type);
-        }
-        return true;
-    }
-
-    private void cleanPrivateFiles(final String type) {
-        try {
-            File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/");
-            File[] array = dir.listFiles();
-            if (array != null) {
-                for (int b = 0; b < array.length; b++) {
-                    String name = array[b].getName().toLowerCase();
-                    if (name.equals(".nomedia")) {
-                        continue;
-                    }
-                    if (array[b].isFile()) {
-                        array[b].delete();
-                    }
-                }
-            }
-        } catch (Throwable e) {
-            Log.e("CleanCache", e.toString());
-        }
-    }
-
-    private boolean deleteOmemoIdentities() {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setTitle(R.string.pref_delete_omemo_identities);
-        final List<CharSequence> accounts = new ArrayList<>();
-        for (Account account : xmppConnectionService.getAccounts()) {
-            if (account.isEnabled()) {
-                accounts.add(account.getJid().asBareJid().toString());
-            }
-        }
-        final boolean[] checkedItems = new boolean[accounts.size()];
-        builder.setMultiChoiceItems(
-                accounts.toArray(new CharSequence[accounts.size()]),
-                checkedItems,
-                (dialog, which, isChecked) -> {
-                    checkedItems[which] = isChecked;
-                    final AlertDialog alertDialog = (AlertDialog) dialog;
-                    for (boolean item : checkedItems) {
-                        if (item) {
-                            alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
-                            return;
-                        }
-                    }
-                    alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
-                });
-        builder.setNegativeButton(R.string.cancel, null);
-        builder.setPositiveButton(
-                R.string.delete_selected_keys,
-                (dialog, which) -> {
-                    for (int i = 0; i < checkedItems.length; ++i) {
-                        if (checkedItems[i]) {
-                            try {
-                                Jid jid = Jid.of(accounts.get(i).toString());
-                                Account account = xmppConnectionService.findAccountByJid(jid);
-                                if (account != null) {
-                                    account.getAxolotlService().regenerateKeys(true);
-                                }
-                            } catch (IllegalArgumentException e) {
-                                //
-                            }
-                        }
-                    }
-                });
-        AlertDialog dialog = builder.create();
-        dialog.show();
-        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
-        return true;
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        PreferenceManager.getDefaultSharedPreferences(this)
-                .unregisterOnSharedPreferenceChangeListener(this);
-    }
-
-    @Override
-    public void onSharedPreferenceChanged(SharedPreferences preferences, String name) {
-        final List<String> resendPresence =
-                Arrays.asList(
-                        "confirm_messages",
-                        DND_ON_SILENT_MODE,
-                        AWAY_WHEN_SCREEN_IS_OFF,
-                        "allow_message_correction",
-                        TREAT_VIBRATE_AS_SILENT,
-                        MANUALLY_CHANGE_PRESENCE,
-                        BROADCAST_LAST_ACTIVITY);
-        if (name.equals(OMEMO_SETTING)) {
-            OmemoSetting.load(this, preferences);
-            changeOmemoSettingSummary();
-        } else if (name.equals(KEEP_FOREGROUND_SERVICE)) {
-            xmppConnectionService.toggleForegroundService();
-        } else if (resendPresence.contains(name)) {
-            if (xmppConnectionServiceBound) {
-                if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) {
-                    xmppConnectionService.toggleScreenEventReceiver();
-                }
-                xmppConnectionService.refreshAllPresences();
-            }
-        } else if (name.equals("dont_trust_system_cas")) {
-            xmppConnectionService.updateMemorizingTrustmanager();
-            reconnectAccounts();
-        } else if (name.equals("use_tor")) {
-            if (preferences.getBoolean(name, false)) {
-                displayToast(getString(R.string.audio_video_disabled_tor));
-            }
-            reconnectAccounts();
-            xmppConnectionService.reinitializeMuclumbusService();
-        } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) {
-            xmppConnectionService.expireOldMessages(true);
-        } else if ( name.equals(THEME) ||
-                    name.equals("custom_theme_automatic") ||
-                    name.equals("custom_theme_dark") ||
-                    name.equals("custom_theme_primary") ||
-                    name.equals("custom_theme_primary_dark") ||
-                    name.equals("custom_theme_accent") ||
-                    name.equals("custom_theme_background_primary") ||
-                    name.equals("custom_dark_theme_primary") ||
-                    name.equals("custom_dark_theme_primary_dark") ||
-                    name.equals("custom_dark_theme_accent") ||
-                    name.equals("custom_dark_theme_background_primary"))
-        {
-            final int theme = findTheme();
-            xmppConnectionService.setTheme(theme);
-            ThemeHelper.applyCustomColors(xmppConnectionService);
-            recreate();
-        } else if (name.equals(PREVENT_SCREENSHOTS)) {
-            SettingsUtils.applyScreenshotPreventionSetting(this);
-        } else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
-            final String pushServerPreference =
-                    Strings.nullToEmpty(preferences.getString(
-                            UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
-                            getString(R.string.default_push_server))).trim();
-            if (isJidInvalid(pushServerPreference) || isHttpUri(pushServerPreference)) {
-                Toast.makeText(this,R.string.invalid_jid,Toast.LENGTH_LONG).show();
-            }
-            if (xmppConnectionService.reconfigurePushDistributor()) {
-                xmppConnectionService.renewUnifiedPushEndpoints();
-            }
-        }
-    }
-
-    private static boolean isJidInvalid(final String input) {
-        if (Strings.isNullOrEmpty(input)) {
-            return true;
-        }
-        try {
-            Jid.ofEscaped(input);
-            return false;
-        } catch (final IllegalArgumentException e) {
-            return true;
-        }
-    }
-
-    private static boolean isHttpUri(final String input) {
-        final URI uri;
-        try {
-            uri = new URI(input);
-        } catch (final URISyntaxException e) {
-            return false;
-        }
-        return Arrays.asList("http","https").contains(uri.getScheme());
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        SettingsUtils.applyScreenshotPreventionSetting(this);
-    }
-
-    @Override
-    public void onRequestPermissionsResult(
-            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-        if (grantResults.length > 0)
-            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-                if (requestCode == REQUEST_CREATE_BACKUP) {
-                    createBackup();
-                }
-                if (requestCode == REQUEST_DOWNLOAD_STICKERS) {
-                    downloadStickers();
-                }
-            } else {
-                Toast.makeText(
-                                this,
-                                getString(
-                                        R.string.no_storage_permission,
-                                        getString(R.string.app_name)),
-                                Toast.LENGTH_SHORT)
-                        .show();
-            }
-    }
-
-    private void createBackup() {
-        new AlertDialog.Builder(this)
-            .setTitle("Create Backup")
-            .setMessage("Export extra Cheogram-only data (backup will not import into other apps then)?")
-            .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
-                createBackup(true);
-            })
-            .setNegativeButton(R.string.no, (dialog, whichButton) -> {
-                createBackup(false);
-            }).show();
-    }
-
-    private void createBackup(boolean withCheogramDb) {
-        Intent intent = new Intent(this, ExportBackupService.class);
-        intent.putExtra("cheogram_db", withCheogramDb);
-        ContextCompat.startForegroundService(this, intent);
-        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setMessage(R.string.backup_started_message);
-        builder.setPositiveButton(R.string.ok, null);
-        builder.create().show();
-    }
-
-    private void downloadStickers() {
-        Intent intent = new Intent(this, DownloadDefaultStickers.class);
-        intent.putExtra("tor", xmppConnectionService.useTorToConnect());
-        ContextCompat.startForegroundService(this, intent);
-        displayToast("Sticker download started");
-    }
-
-    private void displayToast(final String msg) {
-        runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show());
-    }
-
-    private void reconnectAccounts() {
-        for (Account account : xmppConnectionService.getAccounts()) {
-            if (account.isEnabled()) {
-                xmppConnectionService.reconnectAccountInBackground(account);
-            }
-        }
-    }
-
-    public void refreshUiReal() {
-        // nothing to do. This Activity doesn't implement any listeners
-    }
-}

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

@@ -1,104 +0,0 @@
-package eu.siacs.conversations.ui;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceScreen;
-import android.text.TextUtils;
-import android.widget.ListView;
-
-import java.lang.reflect.Method;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.utils.Compatibility;
-
-public class SettingsFragment extends PreferenceFragment {
-
-	private String page = null;
-	private String suffix = null;
-
-	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-
-		addPreferencesFromResource(R.xml.preferences);
-
-		// Remove from standard preferences if the flag ONLY_INTERNAL_STORAGE is false
-		if (!Config.ONLY_INTERNAL_STORAGE) {
-			PreferenceCategory mCategory = (PreferenceCategory) findPreference("security_options");
-			if (mCategory != null) {
-				Preference cleanCache = findPreference("clean_cache");
-				Preference cleanPrivateStorage = findPreference("clean_private_storage");
-				mCategory.removePreference(cleanCache);
-				mCategory.removePreference(cleanPrivateStorage);
-			}
-		}
-		Compatibility.removeUnusedPreferences(this);
-
-		if (!TextUtils.isEmpty(page)) {
-			openPreferenceScreen(page);
-		}
-
-	}
-
-	@Override
-	public void onActivityCreated(Bundle bundle) {
-		super.onActivityCreated(bundle);
-
-		final ListView listView = getActivity().findViewById(android.R.id.list);
-		if (listView != null) {
-			listView.setDivider(null);
-		}
-	}
-
-	public void setActivityIntent(final Intent intent) {
-		boolean wasEmpty = TextUtils.isEmpty(page);
-		if (intent != null) {
-			if (Intent.ACTION_VIEW.equals(intent.getAction())) {
-				if (intent.getExtras() != null) {
-					this.page = intent.getExtras().getString("page");
-					this.suffix = intent.getExtras().getString("suffix");
-					if (wasEmpty) {
-						openPreferenceScreen(page);
-					}
-				}
-			}
-		}
-	}
-
-	private void openPreferenceScreen(final String screenName) {
-		final Preference pref = findPreference(screenName);
-		if (pref instanceof PreferenceScreen) {
-			final PreferenceScreen preferenceScreen = (PreferenceScreen) pref;
-			getActivity().setTitle(preferenceScreen.getTitle());
-			preferenceScreen.setDependency("");
-			if (this.suffix != null) {
-				for (int i = 0; i < preferenceScreen.getPreferenceCount(); i++) {
-					final Preference p = preferenceScreen.getPreference(i);
-					if (!p.hasKey()) continue;
-					p.setKey(p.getKey() + this.suffix);
-					if (p.getDependency() != null && !"".equals(p.getDependency())) {
-						p.setDependency(p.getDependency() + this.suffix);
-					}
-					reloadPref(p);
-				}
-			}
-			setPreferenceScreen((PreferenceScreen) pref);
-		}
-	}
-
-	static void reloadPref(final Preference pref) {
-		Class iterClass = pref.getClass();
-		while(iterClass != Object.class) {
-			try {
-				Method m = iterClass.getDeclaredMethod("onSetInitialValue", boolean.class, Object.class);
-				m.setAccessible(true);
-				m.invoke(pref, true, null);
-			} catch (Exception e) { }
-			iterClass = iterClass.getSuperclass();
-		}
-	}
-}

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

@@ -5,7 +5,6 @@ import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.location.Location;
 import android.location.LocationListener;
-import android.os.Build;
 import android.os.Bundle;
 import android.view.View;
 
@@ -15,11 +14,6 @@ import androidx.databinding.DataBindingUtil;
 import com.google.android.material.snackbar.Snackbar;
 import com.google.common.math.DoubleMath;
 
-import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.util.GeoPoint;
-
-import java.math.RoundingMode;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityShareLocationBinding;
@@ -27,7 +21,11 @@ import eu.siacs.conversations.ui.util.LocationHelper;
 import eu.siacs.conversations.ui.widget.Marker;
 import eu.siacs.conversations.ui.widget.MyLocation;
 import eu.siacs.conversations.utils.LocationProvider;
-import eu.siacs.conversations.utils.ThemeHelper;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.util.GeoPoint;
+
+import java.math.RoundingMode;
 
 public class ShareLocationActivity extends LocationActivity implements LocationListener {
 
@@ -58,6 +56,7 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
         super.onCreate(savedInstanceState);
 
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
         setupMapView(binding.map, LocationProvider.getGeoPoint(this));
@@ -71,13 +70,12 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
         this.snackBar.setAction(R.string.enable, view -> {
             if (isLocationEnabledAndAllowed()) {
                 updateUi();
-            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
+            } else if (!hasLocationPermissions()) {
                 requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
             } else if (!isLocationEnabled()) {
                 startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
             }
         });
-        ThemeHelper.fix(this.snackBar);
 
         this.binding.shareButton.setOnClickListener(this::shareLocation);
 
@@ -87,7 +85,7 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
             if (!marker_fixed_to_loc) {
                 if (!isLocationEnabled()) {
                     startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
-                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                } else {
                     requestPermissions(REQUEST_CODE_FAB_PRESSED);
                 }
             }
@@ -117,16 +115,9 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
                                            @NonNull final int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 
-        if (grantResults.length > 0 &&
-                grantResults[0] != PackageManager.PERMISSION_GRANTED &&
-                Build.VERSION.SDK_INT >= 23 &&
-                permissions.length > 0 &&
-                (
-                        Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
-                                Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
-                                Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
-                ) &&
-                !shouldShowRequestPermissionRationale(permissions[0])) {
+        if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED && permissions.length > 0 && (
+                Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) || Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) || Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
+        ) && !shouldShowRequestPermissionRationale(permissions[0])) {
             noAskAgain = true;
         }
 
@@ -172,7 +163,7 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
     }
 
     @Override
-    public void onLocationChanged(final Location location) {
+    public void onLocationChanged(@NonNull final Location location) {
         if (this.myLoc == null) {
             this.marker_fixed_to_loc = true;
         }
@@ -206,7 +197,7 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
     }
 
     private boolean isLocationEnabledAndAllowed() {
-        return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
+        return this.hasLocationFeature && this.hasLocationPermissions() && this.isLocationEnabled();
     }
 
     private void toggleFixedLocation() {
@@ -229,8 +220,8 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
         if (isLocationEnabledAndAllowed()) {
             this.binding.fab.setVisibility(View.VISIBLE);
             runOnUiThread(() -> {
-                this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
-                        R.drawable.ic_gps_not_fixed_white_24dp);
+                this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_24dp :
+                        R.drawable.ic_gps_not_fixed_24dp);
                 this.binding.fab.setContentDescription(getResources().getString(
                         marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
                 ));

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

@@ -9,21 +9,24 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import java.util.ArrayList;
-import java.util.List;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityShareWithBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.adapter.ConversationAdapter;
 import eu.siacs.conversations.xmpp.Jid;
 
-public class ShareWithActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate {
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShareWithActivity extends XmppActivity
+        implements XmppConnectionService.OnConversationUpdate {
 
     private static final int REQUEST_STORAGE_PERMISSION = 0x733f32;
     private Conversation mPendingConversation = null;
@@ -48,11 +51,10 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
     private ConversationAdapter mAdapter;
     private final List<Conversation> mConversations = new ArrayList<>();
 
-
-    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
+    protected void onActivityResult(
+            final int requestCode, final int resultCode, final Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
-        if (requestCode == REQUEST_START_NEW_CONVERSATION
-                && resultCode == RESULT_OK) {
+        if (requestCode == REQUEST_START_NEW_CONVERSATION && resultCode == RESULT_OK) {
             share.contact = data.getStringExtra("contact");
             share.account = data.getStringExtra(EXTRA_ACCOUNT);
         }
@@ -65,7 +67,10 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
     }
 
     @Override
-    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+    public void onRequestPermissionsResult(
+            final int requestCode,
+            @NonNull final String[] permissions,
+            @NonNull final int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         if (grantResults.length > 0)
             if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@@ -77,27 +82,35 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
                     }
                 }
             } else {
-                Toast.makeText(this, getString(R.string.no_storage_permission, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
+                Toast.makeText(
+                                this,
+                                getString(
+                                        R.string.no_storage_permission,
+                                        getString(R.string.app_name)),
+                                Toast.LENGTH_SHORT)
+                        .show();
             }
     }
 
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
+    protected void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_share_with);
 
-        setSupportActionBar(findViewById(R.id.toolbar));
-        if (getSupportActionBar() != null) {
-            getSupportActionBar().setDisplayHomeAsUpEnabled(false);
-            getSupportActionBar().setHomeButtonEnabled(false);
+        final ActivityShareWithBinding binding =
+                DataBindingUtil.setContentView(this, R.layout.activity_share_with);
+        setSupportActionBar(binding.toolbar);
+        final var actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(false);
+            actionBar.setHomeButtonEnabled(false);
         }
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+        setTitle(R.string.title_activity_share_with);
 
-        setTitle(getString(R.string.title_activity_sharewith));
-
-        RecyclerView mListView = findViewById(R.id.choose_conversation_list);
         mAdapter = new ConversationAdapter(this, this.mConversations);
-        mListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
-        mListView.setAdapter(mAdapter);
+        binding.chooseConversationList.setLayoutManager(
+                new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+        binding.chooseConversationList.setAdapter(mAdapter);
         mAdapter.setConversationClickListener((view, conversation) -> share(conversation));
         this.share = new Share();
     }
@@ -112,8 +125,9 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
     public boolean onOptionsItemSelected(final MenuItem item) {
         switch (item.getItemId()) {
             case R.id.action_add:
-                final Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
-                intent.putExtra("direct_search",true);
+                final Intent intent =
+                        new Intent(getApplicationContext(), ChooseContactActivity.class);
+                intent.putExtra("direct_search", true);
                 startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION);
                 return true;
         }
@@ -133,7 +147,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
         if (Intent.ACTION_SEND.equals(action)) {
             final String text = intent.getStringExtra(Intent.EXTRA_TEXT);
             final Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
-            final boolean asQuote = intent.getBooleanExtra(ConversationsActivity.EXTRA_AS_QUOTE, false);
+            final boolean asQuote =
+                    intent.getBooleanExtra(ConversationsActivity.EXTRA_AS_QUOTE, false);
 
             if (data != null && "geo".equals(data.getScheme())) {
                 this.share.uris.clear();
@@ -151,14 +166,16 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
             this.share.uris = uris == null ? new ArrayList<>() : uris;
         }
         if (xmppConnectionServiceBound) {
-            xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uris.size() == 0, false);
+            xmppConnectionService.populateWithOrderedConversations(
+                    mConversations, this.share.uris.isEmpty(), false);
         }
-
     }
 
     @Override
-    void onBackendConnected() {
-        if (xmppConnectionServiceBound && share != null && ((share.contact != null && share.account != null))) {
+    protected void onBackendConnected() {
+        if (xmppConnectionServiceBound
+                && share != null
+                && ((share.contact != null && share.account != null))) {
             share();
             return;
         }
@@ -167,32 +184,34 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
 
     private void share() {
         final Conversation conversation;
-            Account account;
-            try {
-                account = xmppConnectionService.findAccountByJid(Jid.ofEscaped(share.account));
-            } catch (final IllegalArgumentException e) {
-                account = null;
-            }
-            if (account == null) {
-                return;
-            }
+        Account account;
+        try {
+            account = xmppConnectionService.findAccountByJid(Jid.ofEscaped(share.account));
+        } catch (final IllegalArgumentException e) {
+            account = null;
+        }
+        if (account == null) {
+            return;
+        }
 
-            try {
-                conversation = xmppConnectionService.findOrCreateConversation(account, Jid.of(share.contact), false, true);
-            } catch (final IllegalArgumentException e) {
-                return;
-            }
+        try {
+            conversation =
+                    xmppConnectionService.findOrCreateConversation(
+                            account, Jid.of(share.contact), false, true);
+        } catch (final IllegalArgumentException e) {
+            return;
+        }
         share(conversation);
     }
 
     private void share(final Conversation conversation) {
-        if (share.uris.size() != 0 && !hasStoragePermission(REQUEST_STORAGE_PERMISSION)) {
+        if (!share.uris.isEmpty() && !hasStoragePermission(REQUEST_STORAGE_PERMISSION)) {
             mPendingConversation = conversation;
             return;
         }
-        Intent intent = new Intent(this, ConversationsActivity.class);
+        final Intent intent = new Intent(this, ConversationsActivity.class);
         intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
-        if (share.uris.size() > 0) {
+        if (!share.uris.isEmpty()) {
             intent.setAction(Intent.ACTION_SEND_MULTIPLE);
             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -207,15 +226,20 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
         try {
             startActivity(intent);
         } catch (SecurityException e) {
-            Toast.makeText(this, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show();
+            Toast.makeText(
+                            this,
+                            R.string.sharing_application_not_grant_permission,
+                            Toast.LENGTH_SHORT)
+                    .show();
             return;
         }
         finish();
     }
 
     public void refreshUiReal() {
-        //TODO inject desired order to not resort on refresh
-        xmppConnectionService.populateWithOrderedConversations(mConversations, this.share != null && this.share.uris.size() == 0, false);
+        // TODO inject desired order to not resort on refresh
+        xmppConnectionService.populateWithOrderedConversations(
+                mConversations, this.share != null && this.share.uris.isEmpty(), false);
         mAdapter.notifyDataSetChanged();
     }
 }

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

@@ -9,6 +9,7 @@ import android.location.Location;
 import android.location.LocationListener;
 import android.net.Uri;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
@@ -17,12 +18,8 @@ import android.widget.Toast;
 import androidx.annotation.NonNull;
 import androidx.databinding.DataBindingUtil;
 
-import org.jetbrains.annotations.NotNull;
-import org.osmdroid.util.GeoPoint;
-
-import java.util.HashMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Doubles;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -31,8 +28,13 @@ import eu.siacs.conversations.ui.util.LocationHelper;
 import eu.siacs.conversations.ui.util.UriHelper;
 import eu.siacs.conversations.ui.widget.Marker;
 import eu.siacs.conversations.ui.widget.MyLocation;
+import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.LocationProvider;
 
+import org.osmdroid.util.GeoPoint;
+
+import java.util.Map;
+
 public class ShowLocationActivity extends LocationActivity implements LocationListener {
 
     private GeoPoint loc = LocationProvider.FALLBACK;
@@ -49,79 +51,49 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_show_location);
         setSupportActionBar(binding.toolbar);
 
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+
         configureActionBar(getSupportActionBar());
         setupMapView(this.binding.map, this.loc);
 
         this.binding.fab.setOnClickListener(view -> startNavigation());
 
         final Intent intent = getIntent();
-        if (intent != null) {
-            final String action = intent.getAction();
-            if (action == null) {
-                return;
-            }
-            switch (action) {
-                case "eu.siacs.conversations.location.show":
-                    if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) {
-                        final double longitude = intent.getDoubleExtra("longitude", 0);
-                        final double latitude = intent.getDoubleExtra("latitude", 0);
-                        this.loc = new GeoPoint(latitude, longitude);
-                    }
+        if (intent == null) {
+            return;
+        }
+        final String action = intent.getAction();
+        switch (Strings.nullToEmpty(action)) {
+            case "eu.siacs.conversations.location.show":
+                if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) {
+                    final double longitude = intent.getDoubleExtra("longitude", 0);
+                    final double latitude = intent.getDoubleExtra("latitude", 0);
+                    this.loc = new GeoPoint(latitude, longitude);
+                }
+                break;
+            case Intent.ACTION_VIEW:
+                final Uri uri = intent.getData();
+                if (uri == null) {
                     break;
-                case Intent.ACTION_VIEW:
-                    final Uri geoUri = intent.getData();
-
-                    // Attempt to set zoom level if the geo URI specifies it
-                    if (geoUri != null) {
-                        final HashMap<String, String> query =
-                                UriHelper.parseQueryString(geoUri.getQuery());
-
-                        // Check for zoom level.
-                        final String z = query.get("z");
-                        if (z != null) {
-                            try {
-                                mapController.setZoom(Double.valueOf(z));
-                            } catch (final Exception ignored) {
-                            }
-                        }
-
-                        // Check for the actual geo query.
-                        boolean posInQuery = false;
-                        final String q = query.get("q");
-                        if (q != null) {
-                            final Pattern latlng =
-                                    Pattern.compile(
-                                            "/^([-+]?[0-9]+(\\.[0-9]+)?),([-+]?[0-9]+(\\.[0-9]+)?)(\\(.*\\))?/");
-                            final Matcher m = latlng.matcher(q);
-                            if (m.matches()) {
-                                try {
-                                    this.loc =
-                                            new GeoPoint(
-                                                    Double.valueOf(m.group(1)),
-                                                    Double.valueOf(m.group(3)));
-                                    posInQuery = true;
-                                } catch (final Exception ignored) {
-                                }
-                            }
-                        }
-
-                        final String schemeSpecificPart = geoUri.getSchemeSpecificPart();
-                        if (schemeSpecificPart != null && !schemeSpecificPart.isEmpty()) {
-                            try {
-                                final GeoPoint latlong =
-                                        LocationHelper.parseLatLong(schemeSpecificPart);
-                                if (latlong != null && !posInQuery) {
-                                    this.loc = latlong;
-                                }
-                            } catch (final NumberFormatException ignored) {
-                            }
-                        }
-                    }
-
+                }
+                final GeoPoint point;
+                try {
+                    point = GeoHelper.parseGeoPoint(uri);
+                } catch (final Exception e) {
                     break;
-            }
-            updateLocationMarkers();
+                }
+                this.loc = point;
+                final Map<String, String> query = UriHelper.parseQueryString(uri.getQuery());
+                final String z = query.get("z");
+                final Double zoom = Strings.isNullOrEmpty(z) ? null : Doubles.tryParse(z);
+                if (zoom != null) {
+                    Log.d(Config.LOGTAG, "inferring zoom level " + zoom + " from geo uri");
+                    mapController.setZoom(zoom);
+                    gotoLoc(false);
+                }
+                break;
         }
+        updateLocationMarkers();
     }
 
     @Override
@@ -149,7 +121,7 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
     }
 
     @Override
-    public boolean onCreateOptionsMenu(@NotNull final Menu menu) {
+    public boolean onCreateOptionsMenu(@NonNull final Menu menu) {
         // Inflate the menu; this adds items to the action bar if it is present.
         getMenuInflater().inflate(R.menu.menu_show_location, menu);
         updateUi();
@@ -172,37 +144,43 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
 
     @Override
     public boolean onOptionsItemSelected(final MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.action_copy_location:
-                final ClipboardManager clipboard =
-                        (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
-                if (clipboard != null) {
-                    final ClipData clip =
-                            ClipData.newPlainText("location", createGeoUri().toString());
-                    clipboard.setPrimaryClip(clip);
-                    Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT)
-                            .show();
-                }
-                return true;
-            case R.id.action_share_location:
-                final Intent shareIntent = new Intent();
-                shareIntent.setAction(Intent.ACTION_SEND);
-                shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString());
-                shareIntent.setType("text/plain");
-                try {
-                    startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
-                } catch (final ActivityNotFoundException e) {
-                    // This should happen only on faulty androids because normally chooser is always
-                    // available
-                    Toast.makeText(
-                                    this,
-                                    R.string.no_application_found_to_open_file,
-                                    Toast.LENGTH_SHORT)
-                            .show();
-                }
-                return true;
+        final var itemId = item.getItemId();
+        if (itemId == R.id.action_copy_location) {
+            final ClipboardManager clipboard = getSystemService(ClipboardManager.class);
+            final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString());
+            clipboard.setPrimaryClip(clip);
+            Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
+            return true;
+        } else if (itemId == R.id.action_share_location) {
+            final Intent shareIntent = new Intent();
+            shareIntent.setAction(Intent.ACTION_SEND);
+            shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString());
+            shareIntent.setType("text/plain");
+            try {
+                startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
+            } catch (final ActivityNotFoundException e) {
+                // This should happen only on faulty androids because normally chooser is always
+                // available
+                Toast.makeText(this, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT)
+                        .show();
+            }
+            return true;
+        } else if (itemId == R.id.action_open_with) {
+            final Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setData(createGeoUri());
+            try {
+                startActivity(Intent.createChooser(intent, getText(R.string.open_with)));
+            } catch (final ActivityNotFoundException e) {
+                // This should happen only on faulty androids because normally chooser is always
+                // available
+                Toast.makeText(this, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT)
+                        .show();
+            }
+            return true;
+
+        } else {
+            return super.onOptionsItemSelected(item);
         }
-        return super.onOptionsItemSelected(item);
     }
 
     private void startNavigation() {
@@ -228,7 +206,7 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
     }
 
     @Override
-    public void onLocationChanged(@NotNull final Location location) {
+    public void onLocationChanged(@NonNull final Location location) {
         if (LocationHelper.isBetterLocation(location, this.myLoc)) {
             this.myLoc = location;
             updateLocationMarkers();
@@ -239,8 +217,8 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
     public void onStatusChanged(final String provider, final int status, final Bundle extras) {}
 
     @Override
-    public void onProviderEnabled(@NotNull final String provider) {}
+    public void onProviderEnabled(@NonNull final String provider) {}
 
     @Override
-    public void onProviderDisabled(@NotNull final String provider) {}
+    public void onProviderDisabled(@NonNull final String provider) {}
 }

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

@@ -6,11 +6,10 @@ import android.app.Dialog;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
-import android.graphics.PorterDuff;
+import android.content.res.ColorStateList;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -61,7 +60,10 @@ import androidx.viewpager.widget.ViewPager;
 
 import com.cheogram.android.FinishOnboarding;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
+import com.google.common.collect.Iterables;
 import com.leinardi.android.speeddial.SpeedDialActionItem;
 import com.leinardi.android.speeddial.SpeedDialView;
 
@@ -100,6 +102,7 @@ import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.utils.XmppUri;
+import eu.siacs.conversations.utils.XEP0392Helper;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
@@ -126,7 +129,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     private TagsAdapter mTagsAdapter = new TagsAdapter();
     private final List<ListItem> conferences = new ArrayList<>();
     private ListItemAdapter mConferenceAdapter;
-    private final List<String> mActivatedAccounts = new ArrayList<>();
+    private final ArrayList<String> mActivatedAccounts = new ArrayList<>();
     private EditText mSearchEditText;
     private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
     private final AtomicBoolean mOpenedFab = new AtomicBoolean(false);
@@ -238,19 +241,20 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         }
     };
 
-    public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
-        if (accounts.size() > 0) {
-            ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.simple_list_item, accounts);
-            adapter.setDropDownViewResource(R.layout.simple_list_item);
-            spinner.setAdapter(adapter);
-            spinner.setEnabled(true);
-        } else {
+    public static void populateAccountSpinner(final Context context, final List<String> accounts, final AutoCompleteTextView spinner) {
+        if (accounts.isEmpty()) {
             ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
-                    R.layout.simple_list_item,
+                    R.layout.item_autocomplete,
                     Collections.singletonList(context.getString(R.string.no_accounts)));
-            adapter.setDropDownViewResource(R.layout.simple_list_item);
+            adapter.setDropDownViewResource(R.layout.item_autocomplete);
             spinner.setAdapter(adapter);
             spinner.setEnabled(false);
+        } else {
+            final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts);
+            adapter.setDropDownViewResource(R.layout.item_autocomplete);
+            spinner.setAdapter(adapter);
+            spinner.setEnabled(true);
+            spinner.setText(Iterables.getFirst(accounts,null),false);
         }
     }
 
@@ -291,6 +295,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_start_conversation);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
 
@@ -382,7 +387,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             }
             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))
+                    .setFabImageTintColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorOnSurface))
+                    .setFabBackgroundColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorSurfaceContainerHighest))
                     .create();
             speedDialView.addActionItem(actionItem);
         }
@@ -413,13 +419,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     @Override
     public void onStart() {
         super.onStart();
-        final int theme = findTheme();
-        if (this.mTheme != theme) {
-            recreate();
-        } else {
-            if (pendingViewIntent.peek() == null) {
-                askForContactsPermissions();
-            }
+        if (pendingViewIntent.peek() == null) {
+            askForContactsPermissions();
         }
         mConferenceAdapter.refreshSettings();
         mContactsAdapter.refreshSettings();
@@ -488,7 +489,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         }
         Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true);
         bookmark.setConversation(conversation);
-        if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))) {
+        if (!bookmark.autojoin()) {
             bookmark.setAutojoin(true);
             xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark);
         }
@@ -510,7 +511,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
     protected void deleteContact() {
         final Contact contact = (Contact) contextItem;
-        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setNegativeButton(R.string.cancel, null);
         builder.setTitle(R.string.action_delete_contact);
         builder.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()));
@@ -523,15 +524,23 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
     protected void deleteConference() {
         final Bookmark bookmark = (Bookmark) contextItem;
-
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final var conversation = bookmark.getConversation();
+        final boolean hasConversation = conversation != null;
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setNegativeButton(R.string.cancel, null);
         builder.setTitle(R.string.delete_bookmark);
-        builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_text, bookmark.getJid().toEscapedString()));
-        builder.setPositiveButton(R.string.delete, (dialog, which) -> {
+        if (hasConversation) {
+            builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_and_close, bookmark.getJid().toEscapedString()));
+        } else {
+            builder.setMessage(JidDialog.style(this, R.string.remove_bookmark, bookmark.getJid().toEscapedString()));
+        }
+        builder.setPositiveButton(hasConversation ? R.string.delete_and_close : R.string.delete, (dialog, which) -> {
             bookmark.setConversation(null);
             final Account account = bookmark.getAccount();
             xmppConnectionService.deleteBookmark(account, bookmark);
+            if (conversation != null) {
+                xmppConnectionService.archiveConversation(conversation);
+            }
             filter(mSearchEditText.getText().toString());
         });
         builder.create().show();
@@ -548,7 +557,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         ft.addToBackStack(null);
         EnterJidDialog dialog = EnterJidDialog.newInstance(
                 mActivatedAccounts,
-                getString(R.string.start_conversation),
+                "Start Conversation",
                 getString(R.string.message),
                 "Call",
                 prefilledJid,
@@ -658,18 +667,14 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         dialog.show(ft, FRAGMENT_TAG_DIALOG);
     }
 
-    public static Account getSelectedAccount(Context context, Spinner spinner) {
+    public static Account getSelectedAccount(final Context context, final AutoCompleteTextView spinner) {
         if (spinner == null || !spinner.isEnabled()) {
             return null;
         }
         if (context instanceof XmppActivity) {
-            Jid jid;
+            final Jid jid;
             try {
-                if (Config.DOMAIN_LOCK != null) {
-                    jid = Jid.ofEscaped((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
-                } else {
-                    jid = Jid.ofEscaped((String) spinner.getSelectedItem());
-                }
+                jid = Jid.ofEscaped(spinner.getText().toString());
             } catch (final IllegalArgumentException e) {
                 return null;
             }
@@ -746,7 +751,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         mSearchEditText.setOnEditorActionListener(mSearchDone);
 
         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
-        boolean showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, getResources().getBoolean(R.bool.show_dynamic_tags));
+        boolean showDynamicTags = preferences.getBoolean("show_dynamic_tags", getResources().getBoolean(R.bool.show_dynamic_tags));
         if (showDynamicTags) {
             RecyclerView tags = mSearchView.findViewById(R.id.tags);
             androidx.recyclerview.widget.DividerItemDecoration spacer = new androidx.recyclerview.widget.DividerItemDecoration(tags.getContext(), LinearLayoutManager.HORIZONTAL);
@@ -859,8 +864,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private void askForContactsPermissions() {
-        if (QuickConversationsService.isContactListIntegration(this)
-                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        if (QuickConversationsService.isContactListIntegration(this)) {
             if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
                     != PackageManager.PERMISSION_GRANTED) {
                 if (mRequestedContactsPermission.compareAndSet(false, true)) {
@@ -878,7 +882,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                     if (requiresConsent
                             || shouldShowRequestPermissionRationale(
                                     Manifest.permission.READ_CONTACTS)) {
-                        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+                        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
                         final AtomicBoolean requestPermission = new AtomicBoolean(false);
                         if (QuickConversationsService.isQuicksy()) {
                             builder.setTitle(R.string.quicksy_wants_your_consent);
@@ -1141,7 +1145,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(R.string.verify_omemo_keys);
         View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
         final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
@@ -1281,7 +1285,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     @Override
-    public void onCreateDialogPositiveClick(Spinner spinner, String name) {
+    public void onCreateDialogPositiveClick(AutoCompleteTextView spinner, String name) {
         if (!xmppConnectionServiceBound) {
             return;
         }
@@ -1299,7 +1303,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     @Override
-    public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout layout, AutoCompleteTextView jid, String password, boolean isBookmarkChecked) {
+    public void onJoinDialogPositiveClick(Dialog dialog, AutoCompleteTextView spinner, TextInputLayout layout, AutoCompleteTextView jid, String password, boolean isBookmarkChecked) {
         if (!xmppConnectionServiceBound) {
             return;
         }
@@ -1331,8 +1335,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                 openConversationsForBookmark(bookmark);
             } else {
                 bookmark = new Bookmark(account, conferenceJid.asBareJid());
-                bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin));
                 if (password != null) bookmark.setPassword(password);
+                bookmark.setAutojoin(true);
                 final String nick = conferenceJid.getResource();
                 if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
                     bookmark.setNick(nick);
@@ -1431,7 +1435,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         }
 
         @Override
-        public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) {
+        public void onCreateContextMenu(@NonNull final ContextMenu menu, @NonNull final View v, final ContextMenuInfo menuInfo) {
             super.onCreateContextMenu(menu, v, menuInfo);
             final StartConversationActivity activity = (StartConversationActivity) getActivity();
             if (activity == null) {
@@ -1449,6 +1453,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                 final Bookmark bookmark = (Bookmark) activity.contextItem;
                 final Conversation conversation = bookmark.getConversation();
                 final MenuItem share = menu.findItem(R.id.context_share_uri);
+                final MenuItem delete = menu.findItem(R.id.context_delete_conference);
+                if (conversation != null) {
+                    delete.setTitle(R.string.delete_and_close);
+                } else {
+                    delete.setTitle(R.string.delete_bookmark);
+                }
                 share.setVisible(conversation == null || !conversation.isPrivateAndNonAnonymous());
             } else if (activity.contextItem instanceof Contact) {
                 activity.getMenuInflater().inflate(R.menu.contact_context, menu);
@@ -1643,7 +1653,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
             public void setTag(ListItem.Tag tag) {
                 tv.setText(tag.getName());
-                tv.getBackground().mutate().setColorFilter(tag.getColor(), PorterDuff.Mode.SRC_IN);
+                tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(StartConversationActivity.this,XEP0392Helper.rgbFromNick(tag.getName()))));
             }
         }
 
@@ -1666,7 +1676,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         }
 
         public void setTags(final List<ListItem.Tag> tags) {
-            ListItem.Tag channelTag = new ListItem.Tag("Channel", UIHelper.getColorForName("Channel", true));
+            ListItem.Tag channelTag = new ListItem.Tag("Channel");
             String needle = mSearchEditText == null ? "" : mSearchEditText.getText().toString().toLowerCase(Locale.US).trim();
             HashSet<String> parts = new HashSet<>(Arrays.asList(needle.split("[,\\s]+")));
             this.tags = tags.stream().filter(

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

@@ -61,7 +61,7 @@ public class TimePreference extends DialogPreference implements Preference.OnPre
 		}
 	}
 
-	private static Calendar minutesToCalender(long time) {
+	public static Calendar minutesToCalender(long time) {
 		final Calendar c = Calendar.getInstance();
 		c.set(Calendar.HOUR_OF_DAY, (int) ((time % (24 * 60)) / 60));
 		c.set(Calendar.MINUTE, (int) ((time % (24 * 60)) % 60));

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

@@ -14,14 +14,7 @@ import android.widget.Toast;
 import androidx.appcompat.app.ActionBar;
 import androidx.databinding.DataBindingUtil;
 
-import org.whispersystems.libsignal.IdentityKey;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -40,6 +33,14 @@ import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 
+import org.whispersystems.libsignal.IdentityKey;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdated {
 	private final Map<String, Boolean> ownKeysToTrust = new HashMap<>();
@@ -70,12 +71,14 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 	protected void onCreate(final Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		this.binding = DataBindingUtil.setContentView(this, R.layout.activity_trust_keys);
+		Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 		this.contactJids = new ArrayList<>();
-		for (String jid : getIntent().getStringArrayExtra("contacts")) {
+		final var intent = getIntent();
+		final String[] contacts = intent == null ? null : intent.getStringArrayExtra("contacts");
+		for (final String jid : (contacts == null ? new String[0] : contacts)) {
 			try {
 				this.contactJids.add(Jid.of(jid));
-			} catch (IllegalArgumentException e) {
-				e.printStackTrace();
+			} catch (final IllegalArgumentException ignored) {
 			}
 		}
 
@@ -100,7 +103,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 	public boolean onCreateOptionsMenu(Menu menu) {
 		getMenuInflater().inflate(R.menu.trust_keys, menu);
 		MenuItem scanQrCode = menu.findItem(R.id.action_scan_qr_code);
-		scanQrCode.setVisible((ownKeysToTrust.size() > 0 || foreignActuallyHasKeys()) && isCameraFeatureAvailable());
+		scanQrCode.setVisible((!ownKeysToTrust.isEmpty() || foreignActuallyHasKeys()) && isCameraFeatureAvailable());
 		return super.onCreateOptionsMenu(menu);
 	}
 
@@ -152,7 +155,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 		} else {
 			reloadFingerprints();
 			Log.d(Config.LOGTAG, "xmpp uri was: " + uri.getJid() + " has Fingerprints: " + uri.hasFingerprints());
-			Toast.makeText(this, R.string.barcode_does_not_contain_fingerprints_for_this_conversation, Toast.LENGTH_SHORT).show();
+			Toast.makeText(this, R.string.barcode_does_not_contain_fingerprints_for_this_chat, Toast.LENGTH_SHORT).show();
 		}
 		populateView();
 	}
@@ -195,7 +198,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 							}
 					);
 				}
-				if (fingerprints.size() == 0) {
+				if (fingerprints.isEmpty()) {
 					keysCardBinding.noKeysToAccept.setVisibility(View.VISIBLE);
 					if (hasNoOtherTrustedKeys(jid)) {
 						if (!mAccount.getRoster().getContact(jid).mutualPresenceSubscription()) {
@@ -258,8 +261,8 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 		}
 	}
 
-	private void disableEncryptionDialog(View view) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+	private void disableEncryptionDialog(final View view) {
+		final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 		builder.setTitle(R.string.disable_encryption);
 		builder.setMessage(R.string.disable_encryption_message);
 		builder.setPositiveButton(R.string.disable_now, (dialog, which) -> {
@@ -283,7 +286,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 	private boolean foreignActuallyHasKeys() {
 		synchronized (this.foreignKeysToTrust) {
 			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
-				if (entry.getValue().size() > 0) {
+				if (!entry.getValue().isEmpty()) {
 					return true;
 				}
 			}
@@ -309,7 +312,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 			foreignKeysToTrust.clear();
 			for (Jid jid : contactJids) {
 				Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid);
-				if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
+				if (hasNoOtherTrustedKeys(jid) && ownKeysSet.isEmpty()) {
 					foreignKeysSet.addAll(service.getKeysWithTrust(FingerprintStatus.createActive(false), jid));
 				}
 				Map<String, Boolean> foreignFingerprints = new HashMap<>();
@@ -319,7 +322,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 						foreignFingerprints.put(fingerprint, false);
 					}
 				}
-				if (foreignFingerprints.size() > 0 || !acceptedTargets.contains(jid)) {
+				if (!foreignFingerprints.isEmpty() || !acceptedTargets.contains(jid)) {
 					foreignKeysToTrust.put(jid, foreignFingerprints);
 				}
 			}

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

@@ -12,8 +12,8 @@ import android.util.Log;
 import android.view.View;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
-import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
 
@@ -45,14 +45,12 @@ 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 class UriHandlerActivity extends BaseActivity {
 
     public static final String ACTION_SCAN_QR_CODE = "scan_qr_code";
     private static final String EXTRA_ALLOW_PROVISIONING = "extra_allow_provisioning";
@@ -266,13 +264,13 @@ public class UriHandlerActivity extends AppCompatActivity {
         this.call.enqueue(
                 new Callback() {
                     @Override
-                    public void onFailure(@NotNull Call call, @NotNull IOException e) {
+                    public void onFailure(@NonNull Call call, @NonNull 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) {
+                    public void onResponse(@NonNull Call call, @NonNull Response response) {
                         if (response.isSuccessful()) {
                             final String linkHeader = response.header("Link");
                             if (linkHeader != null && processLinkHeader(linkHeader)) {

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

@@ -4,7 +4,6 @@ import android.telephony.TelephonyManager;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
@@ -19,10 +18,9 @@ import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.content.res.TypedArray;
 import android.graphics.Bitmap;
-import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.drawable.AnimatedImageDrawable;
 import android.graphics.drawable.BitmapDrawable;
@@ -55,10 +53,11 @@ import androidx.annotation.BoolRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AlertDialog.Builder;
 import androidx.appcompat.app.AppCompatDelegate;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.common.base.Strings;
 
 import java.io.IOException;
@@ -69,6 +68,7 @@ import java.util.List;
 import java.util.PriorityQueue;
 import java.util.concurrent.RejectedExecutionException;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -170,7 +170,6 @@ public abstract class XmppActivity extends ActionBarActivity {
 
         }
     };
-    public boolean mSkipBackgroundBinding = false;
 
     public static boolean cancelPotentialWork(Message message, ImageView imageView) {
         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
@@ -228,14 +227,13 @@ public abstract class XmppActivity extends ActionBarActivity {
     abstract protected void refreshUiReal();
 
     @Override
-    protected void onStart() {
+    public void onStart() {
         super.onStart();
+        if (!this.mCustomColors.equals(ThemeHelper.applyCustomColors(this))) {
+            recreate();
+        }
         if (!xmppConnectionServiceBound) {
-            if (this.mSkipBackgroundBinding) {
-                Log.d(Config.LOGTAG, "skipping background binding");
-            } else {
-                connectToBackend();
-            }
+            connectToBackend();
         } else {
             this.registerListeners();
             this.onBackendConnected();
@@ -271,7 +269,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     public void showInstallPgpDialog() {
-        Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(getString(R.string.openkeychain_required));
         builder.setIconAttribute(android.R.attr.alertDialogIcon);
         builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name))));
@@ -314,7 +312,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     protected void deleteAccount(final Account account, final Runnable postDelete) {
-        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
         final CheckBox deleteFromServer =
                 dialogView.findViewById(R.id.delete_from_server);
@@ -371,7 +369,7 @@ public abstract class XmppActivity extends ActionBarActivity {
         dialog.show();
     }
 
-    abstract void onBackendConnected();
+    protected abstract void onBackendConnected();
 
     protected void registerListeners() {
         if (this instanceof XmppConnectionService.OnConversationUpdate) {
@@ -437,7 +435,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     public boolean onOptionsItemSelected(final MenuItem item) {
         switch (item.getItemId()) {
             case R.id.action_settings:
-                startActivity(new Intent(this, SettingsActivity.class));
+                startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
                 break;
             case R.id.action_privacy_policy:
                 openPrivacyPolicy();
@@ -508,11 +506,8 @@ public abstract class XmppActivity extends ActionBarActivity {
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         metrics = getResources().getDisplayMetrics();
-        ExceptionHelper.init(getApplicationContext());
         EmojiInitializationService.execute(this);
         this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
-        this.mTheme = findTheme();
-        setTheme(this.mTheme);
         this.mCustomColors = ThemeHelper.applyCustomColors(this);
     }
 
@@ -520,20 +515,6 @@ public abstract class XmppActivity extends ActionBarActivity {
         return this.isCameraFeatureAvailable;
     }
 
-    public boolean isDarkTheme() {
-        return ThemeHelper.isDark(mTheme);
-    }
-
-    public int getThemeResource(int r_attr_name, int r_drawable_def) {
-        int[] attrs = {r_attr_name};
-        TypedArray ta = this.getTheme().obtainStyledAttributes(attrs);
-
-        int res = ta.getResourceId(0, r_drawable_def);
-        ta.recycle();
-
-        return res;
-    }
-
     protected boolean isOptimizingBattery() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
@@ -738,21 +719,10 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
     }
 
-    @SuppressWarnings("deprecation")
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
-    protected void setListItemBackgroundOnView(View view) {
-        int sdk = android.os.Build.VERSION.SDK_INT;
-        if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
-            view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
-        } else {
-            view.setBackground(getResources().getDrawable(R.drawable.greybackground));
-        }
-    }
-
-    protected void choosePgpSignId(Account account) {
-        xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
+    protected void choosePgpSignId(final Account account) {
+        xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<>() {
             @Override
-            public void success(Account account1) {
+            public void success(final Account a) {
             }
 
             @Override
@@ -773,8 +743,7 @@ public abstract class XmppActivity extends ActionBarActivity {
 
     protected void displayErrorDialog(final int errorCode) {
         runOnUiThread(() -> {
-            Builder builder = new Builder(XmppActivity.this);
-            builder.setIconAttribute(android.R.attr.alertDialogIcon);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(XmppActivity.this);
             builder.setTitle(getString(R.string.error));
             builder.setMessage(errorCode);
             builder.setNeutralButton(R.string.accept, null);
@@ -784,7 +753,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     protected void showAddToRosterDialog(final Contact contact) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(contact.getJid().toString());
         builder.setMessage(getString(R.string.not_in_roster));
         builder.setNegativeButton(getString(R.string.cancel), null);
@@ -796,7 +765,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     private void showAskForPresenceDialog(final Contact contact) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(contact.getJid().toString());
         builder.setMessage(R.string.request_presence_updates);
         builder.setNegativeButton(R.string.cancel, null);
@@ -835,8 +804,8 @@ public abstract class XmppActivity extends ActionBarActivity {
                            boolean password,
                            boolean permitEmpty,
                            boolean alwaysCallback) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+        final DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false);
         if (password) {
             binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
         }
@@ -877,7 +846,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     protected boolean hasStoragePermission(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.TIRAMISU) {
             if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                 requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
                 return false;
@@ -937,7 +906,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     protected boolean manuallyChangePresence() {
-        return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
+        return getBooleanPreference(AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
     }
 
     protected String getShareableUri() {
@@ -978,11 +947,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     @Override
     protected void onResume(){
         super.onResume();
-        SettingsUtils.applyScreenshotPreventionSetting(this);
-    }
-
-    protected int findTheme() {
-        return ThemeHelper.find(this);
+        SettingsUtils.applyScreenshotSetting(this);
     }
 
     @Override
@@ -1006,14 +971,23 @@ public abstract class XmppActivity extends ActionBarActivity {
         if (uri == null || uri.isEmpty()) {
             return;
         }
-        Point size = new Point();
+        final Point size = new Point();
         getWindowManager().getDefaultDisplay().getSize(size);
-        final int width = (size.x < size.y ? size.x : size.y);
-        Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width);
-        ImageView view = new ImageView(this);
-        view.setBackgroundColor(Color.WHITE);
+        final int width = Math.min(size.x, size.y);
+        final int black;
+        final int white;
+        if (Activities.isNightMode(this)) {
+            black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured");
+            white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured");
+        } else {
+            black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured");
+            white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured");
+        }
+        final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
+        final ImageView view = new ImageView(this);
+        view.setBackgroundColor(white);
         view.setImageBitmap(bitmap);
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setView(view);
         builder.create().show();
     }
@@ -1080,14 +1054,15 @@ public abstract class XmppActivity extends ActionBarActivity {
             return invite;
         }
 
-        public boolean execute(XmppActivity activity) {
-            XmppConnectionService service = activity.xmppConnectionService;
-            Conversation conversation = service.findConversationByUuid(this.uuid);
+        public boolean execute(final XmppActivity activity) {
+            final XmppConnectionService service = activity.xmppConnectionService;
+            final Conversation conversation = service.findConversationByUuid(this.uuid);
             if (conversation == null) {
                 return false;
             }
             if (conversation.getMode() == Conversation.MODE_MULTI) {
-                for (Jid jid : jids) {
+                for (final Jid jid : jids) {
+                    // TODO use direct invites for public conferences
                     service.invite(conversation, jid);
                 }
                 return false;
@@ -1181,4 +1156,9 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
         return null;
     }
+
+    public boolean isDark() {
+        int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+        return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
+    }
 }

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

@@ -0,0 +1,70 @@
+package eu.siacs.conversations.ui.activity;
+
+import android.app.Notification;
+import android.os.Bundle;
+
+import androidx.databinding.DataBindingUtil;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.google.common.collect.ImmutableSet;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivitySettingsBinding;
+import eu.siacs.conversations.ui.Activities;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.fragment.settings.MainSettingsFragment;
+import eu.siacs.conversations.ui.fragment.settings.NotificationsSettingsFragment;
+import eu.siacs.conversations.ui.fragment.settings.XmppPreferenceFragment;
+
+import java.util.Collections;
+
+public class SettingsActivity extends XmppActivity {
+
+    @Override
+    protected void refreshUiReal() {}
+
+    @Override
+    protected void onBackendConnected() {
+        final var fragmentManager = getSupportFragmentManager();
+        final var currentFragment = fragmentManager.findFragmentById(R.id.fragment_container);
+        if (currentFragment instanceof XmppPreferenceFragment xmppPreferenceFragment) {
+            xmppPreferenceFragment.onBackendConnected();
+        }
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final ActivitySettingsBinding binding =
+                DataBindingUtil.setContentView(this, R.layout.activity_settings);
+        setSupportActionBar(binding.materialToolbar);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+
+        final var intent = getIntent();
+        final var categories = intent == null ? Collections.emptySet() : intent.getCategories();
+        final PreferenceFragmentCompat preferenceFragment;
+        if (ImmutableSet.of(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
+                .equals(categories)) {
+            preferenceFragment = new NotificationsSettingsFragment();
+        } else {
+            preferenceFragment = new MainSettingsFragment();
+        }
+
+        final var fragmentManager = getSupportFragmentManager();
+        final var currentFragment = fragmentManager.findFragmentById(R.id.fragment_container);
+        if (currentFragment == null) {
+            fragmentManager
+                    .beginTransaction()
+                    .replace(R.id.fragment_container, preferenceFragment)
+                    .commit();
+        }
+        binding.materialToolbar.setNavigationOnClickListener(
+                view -> {
+                    if (fragmentManager.getBackStackEntryCount() == 0) {
+                        finish();
+                    } else {
+                        fragmentManager.popBackStack();
+                    }
+                });
+    }
+}

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

@@ -0,0 +1,47 @@
+package eu.siacs.conversations.ui.activity.result;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class PickRingtone extends ActivityResultContract<Uri, Uri> {
+
+    private static final Uri NONE = Uri.parse("about:blank");
+
+    private final int ringToneType;
+
+    public PickRingtone(final int ringToneType) {
+        this.ringToneType = ringToneType;
+    }
+
+    @NonNull
+    @Override
+    public Intent createIntent(@NonNull final Context context, final Uri existing) {
+        final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
+        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringToneType);
+        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
+        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
+        if (existing != null) {
+            intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existing);
+        }
+        return intent;
+    }
+
+    @Override
+    public Uri parseResult(int resultCode, @Nullable Intent data) {
+        if (resultCode != Activity.RESULT_OK || data == null) {
+            return null;
+        }
+        final Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
+        return pickedUri == null ? NONE : pickedUri;
+    }
+
+    public static Uri noneToNull(final Uri uri) {
+        return uri == null || NONE.equals(uri) ? null : uri;
+    }
+}

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

@@ -9,17 +9,18 @@ import androidx.annotation.NonNull;
 import androidx.core.graphics.ColorUtils;
 import androidx.databinding.DataBindingUtil;
 
-import java.util.List;
+import com.google.android.material.color.MaterialColors;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.AccountRowBinding;
+import eu.siacs.conversations.databinding.ItemAccountBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.utils.UIHelper;
 
+import java.util.List;
+
 public class AccountAdapter extends ArrayAdapter<Account> {
 
     private final XmppActivity activity;
@@ -37,36 +38,33 @@ public class AccountAdapter extends ArrayAdapter<Account> {
         this.showStateButton = true;
     }
 
+    @NonNull
     @Override
     public View getView(int position, View view, @NonNull ViewGroup parent) {
         final Account account = getItem(position);
         final ViewHolder viewHolder;
         if (view == null) {
-            AccountRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.account_row, parent, false);
+            ItemAccountBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_account, parent, false);
             view = binding.getRoot();
             viewHolder = new ViewHolder(binding);
             view.setTag(viewHolder);
         } else {
             viewHolder = (ViewHolder) view.getTag();
         }
-        if (Config.DOMAIN_LOCK != null) {
-            viewHolder.binding.accountJid.setText(account.getJid().getLocal());
-        } else {
-            viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toEscapedString());
-        }
+        viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toEscapedString());
         AvatarWorkerTask.loadAvatar(account, viewHolder.binding.accountImage, R.dimen.avatar);
         viewHolder.binding.accountStatus.setText(getContext().getString(account.getStatus().getReadableId()));
         switch (account.getStatus()) {
             case ONLINE:
-                viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
+                viewHolder.binding.accountStatus.setTextColor(MaterialColors.getColor(viewHolder.binding.accountStatus, com.google.android.material.R.attr.colorPrimary));
                 break;
             case DISABLED:
             case LOGGED_OUT:
             case CONNECTING:
-                viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
+                viewHolder.binding.accountStatus.setTextColor(MaterialColors.getColor(viewHolder.binding.accountStatus, com.google.android.material.R.attr.colorOnSurfaceVariant));
                 break;
             default:
-                viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
+                viewHolder.binding.accountStatus.setTextColor(MaterialColors.getColor(viewHolder.binding.accountStatus, com.google.android.material.R.attr.colorError));
                 break;
         }
         if (account.isOnlineAndConnected()) {
@@ -97,15 +95,15 @@ public class AccountAdapter extends ArrayAdapter<Account> {
             }
         });
         if (activity.xmppConnectionService != null && activity.xmppConnectionService.getAccounts().size() > 1) {
-            viewHolder.binding.frame.setBackgroundColor(account.getColor(activity.isDarkTheme()));
+            viewHolder.binding.frame.setBackgroundColor(account.getColor(activity.isDark()));
         }
         return view;
     }
 
     private static class ViewHolder {
-        private final AccountRowBinding binding;
+        private final ItemAccountBinding binding;
 
-        private ViewHolder(AccountRowBinding binding) {
+        private ViewHolder(ItemAccountBinding binding) {
             this.binding = binding;
         }
     }

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

@@ -13,18 +13,18 @@ import androidx.recyclerview.widget.DiffUtil;
 import androidx.recyclerview.widget.ListAdapter;
 import androidx.recyclerview.widget.RecyclerView;
 
-import java.util.Locale;
-
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.SearchResultItemBinding;
+import eu.siacs.conversations.databinding.ItemChannelDiscoveryBinding;
 import eu.siacs.conversations.entities.Room;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.xmpp.Jid;
 
+import java.util.Locale;
+
 public class ChannelSearchResultAdapter extends ListAdapter<Room, ChannelSearchResultAdapter.ViewHolder> implements View.OnCreateContextMenuListener {
 
-    private static final DiffUtil.ItemCallback<Room> DIFF = new DiffUtil.ItemCallback<Room>() {
+    private static final DiffUtil.ItemCallback<Room> DIFF = new DiffUtil.ItemCallback<>() {
         @Override
         public boolean areItemsTheSame(@NonNull Room a, @NonNull Room b) {
             return a.address != null && a.address.equals(b.address);
@@ -45,7 +45,7 @@ public class ChannelSearchResultAdapter extends ListAdapter<Room, ChannelSearchR
     @NonNull
     @Override
     public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
-        return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.search_result_item, viewGroup, false));
+        return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_channel_discovery, viewGroup, false));
     }
 
     @Override
@@ -99,9 +99,9 @@ public class ChannelSearchResultAdapter extends ListAdapter<Room, ChannelSearchR
 
     public static class ViewHolder extends RecyclerView.ViewHolder {
 
-        private final SearchResultItemBinding binding;
+        private final ItemChannelDiscoveryBinding binding;
 
-        private ViewHolder(SearchResultItemBinding binding) {
+        private ViewHolder(final ItemChannelDiscoveryBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
         }

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

@@ -1,5 +1,6 @@
 package eu.siacs.conversations.ui.adapter;
 
+import android.content.res.ColorStateList;
 import android.graphics.Typeface;
 import android.util.Pair;
 import android.view.MenuItem;
@@ -7,31 +8,32 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.core.graphics.ColorUtils;
+import androidx.core.widget.ImageViewCompat;
 import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.google.android.material.color.MaterialColors;
 import com.google.common.base.Optional;
-import com.google.common.base.Strings;
-
-import java.util.List;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.ConversationListRowBinding;
+import eu.siacs.conversations.databinding.ItemConversationBinding;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.ui.ConversationFragment;
 import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.util.Attachment;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.utils.IrregularUnicodeDetector;
-import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
 
+import java.util.List;
+
 public class ConversationAdapter
         extends RecyclerView.Adapter<ConversationAdapter.ConversationViewHolder> {
 
@@ -50,7 +52,7 @@ public class ConversationAdapter
         return new ConversationViewHolder(
                 DataBindingUtil.inflate(
                         LayoutInflater.from(parent.getContext()),
-                        R.layout.conversation_list_row,
+                        R.layout.item_conversation,
                         parent,
                         false));
     }
@@ -69,13 +71,55 @@ public class ConversationAdapter
             viewHolder.binding.conversationName.setText(name);
         }
 
-        if (activity.xmppConnectionService != null && activity.xmppConnectionService.getAccounts().size() > 1) {
-            viewHolder.binding.frame.setBackgroundColor(conversation.getAccount().getColor(activity.isDarkTheme()));
+        if (conversation == ConversationFragment.getConversation(activity)) {
+            viewHolder.binding.frame.setBackgroundResource(
+                    R.drawable.background_selected_item_conversation);
+            viewHolder.binding.frame.setBackgroundColor(MaterialColors.getColor(viewHolder.binding.frame, com.google.android.material.R.attr.colorSurfaceDim));
+        } else {
+            if (activity.xmppConnectionService != null && activity.xmppConnectionService.getAccounts().size() > 1) {
+                viewHolder.binding.frame.setBackgroundColor(conversation.getAccount().getColor(activity.isDark()));
+            } else {
+                viewHolder.binding.frame.setBackgroundColor(
+                    MaterialColors.getColor(
+                            viewHolder.binding.frame,
+                            android.R.attr.colorBackground));
+            }
         }
 
-        Message message = conversation.getLatestMessage();
+        final Message message = conversation.getLatestMessage();
+        final int status = message.getStatus();
         final int unreadCount = conversation.unreadCount();
         final boolean isRead = conversation.isRead();
+        final @DrawableRes Integer messageStatusDrawable =
+                MessageAdapter.getMessageStatusAsDrawable(message, status);
+        if (message.getType() == Message.TYPE_RTP_SESSION) {
+            viewHolder.binding.messageStatus.setVisibility(View.GONE);
+        } else if (messageStatusDrawable == null) {
+            if (status <= Message.STATUS_RECEIVED) {
+                viewHolder.binding.messageStatus.setVisibility(View.GONE);
+            } else {
+                viewHolder.binding.messageStatus.setVisibility(View.INVISIBLE);
+            }
+        } else {
+            viewHolder.binding.messageStatus.setImageResource(messageStatusDrawable);
+            if (status == Message.STATUS_SEND_DISPLAYED) {
+                viewHolder.binding.messageStatus.setImageResource(R.drawable.ic_done_all_bold_24dp);
+                ImageViewCompat.setImageTintList(
+                        viewHolder.binding.messageStatus,
+                        ColorStateList.valueOf(
+                                MaterialColors.getColor(
+                                        viewHolder.binding.messageStatus,
+                                        com.google.android.material.R.attr.colorPrimary)));
+            } else {
+                ImageViewCompat.setImageTintList(
+                        viewHolder.binding.messageStatus,
+                        ColorStateList.valueOf(
+                                MaterialColors.getColor(
+                                        viewHolder.binding.messageStatus,
+                                        com.google.android.material.R.attr.colorControlNormal)));
+            }
+            viewHolder.binding.messageStatus.setVisibility(View.VISIBLE);
+        }
         final Conversation.Draft draft = isRead ? conversation.getDraft() : null;
         if (unreadCount > 0) {
             viewHolder.binding.unreadCount.setVisibility(View.VISIBLE);
@@ -104,68 +148,9 @@ public class ConversationAdapter
                     && (message.isFileOrImage()
                             || message.treatAsDownloadable()
                             || message.isGeoUri())) {
-                final int imageResource;
-                if (message.isGeoUri()) {
-                    imageResource =
-                            activity.getThemeResource(
-                                    R.attr.ic_attach_location, R.drawable.ic_attach_location);
-                    showPreviewText = false;
-                } else {
-                    // TODO move this into static MediaPreview method and use same icons as in
-                    // MediaAdapter
-                    final String mime = message.getMimeType();
-                    if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) {
-                        final Message.FileParams fileParams = message.getFileParams();
-                        if (fileParams.width > 0 && fileParams.height > 0) {
-                            imageResource =
-                                    activity.getThemeResource(
-                                            R.attr.ic_attach_videocam,
-                                            R.drawable.ic_attach_videocam);
-                            showPreviewText = false;
-                        } else if (fileParams.runtime > 0) {
-                            imageResource =
-                                    activity.getThemeResource(
-                                            R.attr.ic_attach_record, R.drawable.ic_attach_record);
-                            showPreviewText = false;
-                        } else {
-                            imageResource =
-                                    activity.getThemeResource(
-                                            R.attr.ic_attach_document,
-                                            R.drawable.ic_attach_document);
-                            showPreviewText = true;
-                        }
-                    } else {
-                        switch (Strings.nullToEmpty(mime).split("/")[0]) {
-                            case "image":
-                                imageResource =
-                                        activity.getThemeResource(
-                                                R.attr.ic_attach_photo, R.drawable.ic_attach_photo);
-                                showPreviewText = false;
-                                break;
-                            case "video":
-                                imageResource =
-                                        activity.getThemeResource(
-                                                R.attr.ic_attach_videocam,
-                                                R.drawable.ic_attach_videocam);
-                                showPreviewText = false;
-                                break;
-                            case "audio":
-                                imageResource =
-                                        activity.getThemeResource(
-                                                R.attr.ic_attach_record,
-                                                R.drawable.ic_attach_record);
-                                showPreviewText = false;
-                                break;
-                            default:
-                                imageResource =
-                                        activity.getThemeResource(
-                                                R.attr.ic_attach_document,
-                                                R.drawable.ic_attach_document);
-                                showPreviewText = true;
-                                break;
-                        }
-                    }
-                }
+                final var attachment = Attachment.of(message);
+                final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
+                showPreviewText = false;
                 viewHolder.binding.conversationLastmsgImg.setImageResource(imageResource);
                 viewHolder.binding.conversationLastmsgImg.setVisibility(View.VISIBLE);
             } else {
@@ -201,7 +186,7 @@ public class ConversationAdapter
                     viewHolder.binding.senderName.setTypeface(null, Typeface.BOLD);
                 }
             }
-            if (message.getStatus() == Message.STATUS_RECEIVED) {
+            if (status == Message.STATUS_RECEIVED) {
                 if (conversation.getMode() == Conversation.MODE_MULTI) {
                     viewHolder.binding.senderName.setVisibility(View.VISIBLE);
                     final String dname = UIHelper.getMessageDisplayName(message);
@@ -230,36 +215,25 @@ public class ConversationAdapter
 
         if (ongoingCall.isPresent()) {
             viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
-            final int ic_ongoing_call =
-                    activity.getThemeResource(
-                            R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp);
-            viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call);
+            viewHolder.binding.notificationStatus.setImageResource(
+                    R.drawable.ic_phone_in_talk_24dp);
         } else {
             final long muted_till =
                     conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
             if (muted_till == Long.MAX_VALUE) {
                 viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
-                int ic_notifications_off =
-                        activity.getThemeResource(
-                                R.attr.icon_notifications_off,
-                                R.drawable.ic_notifications_off_black_24dp);
-                viewHolder.binding.notificationStatus.setImageResource(ic_notifications_off);
+                viewHolder.binding.notificationStatus.setImageResource(
+                        R.drawable.ic_notifications_off_24dp);
             } else if (muted_till >= System.currentTimeMillis()) {
                 viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
-                int ic_notifications_paused =
-                        activity.getThemeResource(
-                                R.attr.icon_notifications_paused,
-                                R.drawable.ic_notifications_paused_black_24dp);
-                viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused);
+                viewHolder.binding.notificationStatus.setImageResource(
+                        R.drawable.ic_notifications_paused_24dp);
             } else if (conversation.alwaysNotify()) {
                 viewHolder.binding.notificationStatus.setVisibility(View.GONE);
             } else {
                 viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
-                int ic_notifications_none =
-                        activity.getThemeResource(
-                                R.attr.icon_notifications_none,
-                                R.drawable.ic_notifications_none_black_24dp);
-                viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none);
+                viewHolder.binding.notificationStatus.setImageResource(
+                        R.drawable.ic_notifications_none_24dp);
             }
         }
 
@@ -306,9 +280,9 @@ public class ConversationAdapter
     }
 
     static class ConversationViewHolder extends RecyclerView.ViewHolder {
-        private final ConversationListRowBinding binding;
+        private final ItemConversationBinding binding;
 
-        private ConversationViewHolder(ConversationListRowBinding binding) {
+        private ConversationViewHolder(final ItemConversationBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
             binding.getRoot().setLongClickable(true);

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

@@ -6,32 +6,35 @@ import android.widget.Filter;
 
 import androidx.annotation.NonNull;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+
+import eu.siacs.conversations.Config;
+
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.List;
 import java.util.Locale;
 import java.util.regex.Pattern;
 
-import eu.siacs.conversations.Config;
-
 public class KnownHostsAdapter extends ArrayAdapter<String> {
 
     private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$");
 
-    private ArrayList<String> domains;
+    private List<String> domains;
     private final Filter domainFilter = new Filter() {
 
         @Override
-        protected FilterResults performFiltering(CharSequence constraint) {
-            final ArrayList<String> suggestions = new ArrayList<>();
+        protected FilterResults performFiltering(final CharSequence constraint) {
+            final ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
             final String[] split = constraint == null ? new String[0] : constraint.toString().split("@");
             if (split.length == 1) {
                 final String local = split[0].toLowerCase(Locale.ENGLISH);
                 if (Config.QUICKSY_DOMAIN != null && E164_PATTERN.matcher(local).matches()) {
-                    suggestions.add(local + '@' + Config.QUICKSY_DOMAIN.toEscapedString());
+                    builder.add(local + '@' + Config.QUICKSY_DOMAIN.toEscapedString());
                 } else {
                     for (String domain : domains) {
-                        suggestions.add(local + '@' + domain);
+                        builder.add(local + '@' + domain);
                     }
                 }
             } else if (split.length == 2) {
@@ -40,45 +43,49 @@ public class KnownHostsAdapter extends ArrayAdapter<String> {
                 if (domains.contains(domainPart)) {
                     return new FilterResults();
                 }
-                for (String domain : domains) {
+                for (final String domain : domains) {
                     if (domain.contains(domainPart)) {
-                        suggestions.add(localPart + "@" + domain);
+                        builder.add(localPart + "@" + domain);
                     }
                 }
             } else {
                 return new FilterResults();
             }
-            FilterResults filterResults = new FilterResults();
+            final var suggestions = builder.build();
+            final FilterResults filterResults = new FilterResults();
             filterResults.values = suggestions;
             filterResults.count = suggestions.size();
             return filterResults;
         }
 
         @Override
-        protected void publishResults(CharSequence constraint, FilterResults results) {
-            ArrayList filteredList = (ArrayList) results.values;
-            if (results.count > 0) {
-                clear();
-                addAll(filteredList);
-                notifyDataSetChanged();
+        protected void publishResults(final CharSequence constraint, final FilterResults results) {
+            final ImmutableList.Builder<String> suggestions = new ImmutableList.Builder<>();
+            if (results.values instanceof Collection<?> collection) {
+                for(final Object item : collection) {
+                    if (item instanceof String string) {
+                        suggestions.add(string);
+                    }
+                }
             }
+            clear();
+            addAll(suggestions.build());
+            notifyDataSetChanged();
         }
     };
 
-    public KnownHostsAdapter(Context context, int viewResourceId, Collection<String> mKnownHosts) {
+    public KnownHostsAdapter(final Context context, final int viewResourceId, final Collection<String> knownHosts) {
         super(context, viewResourceId, new ArrayList<>());
-        domains = new ArrayList<>(mKnownHosts);
-        Collections.sort(domains);
+        domains =  Ordering.natural().sortedCopy(knownHosts);
     }
 
-    public KnownHostsAdapter(Context context, int viewResourceId) {
+    public KnownHostsAdapter(final Context context, final int viewResourceId) {
         super(context, viewResourceId, new ArrayList<>());
-        domains = new ArrayList<>();
+        domains = ImmutableList.of();
     }
 
-    public void refresh(Collection<String> knownHosts) {
-        domains = new ArrayList<>(knownHosts);
-        Collections.sort(domains);
+    public void refresh(final Collection<String> knownHosts) {
+        this.domains = Ordering.natural().sortedCopy(knownHosts);
         notifyDataSetChanged();
     }
 

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

@@ -1,8 +1,9 @@
 package eu.siacs.conversations.ui.adapter;
 
 import android.content.SharedPreferences;
-import android.graphics.PorterDuff;
+import android.content.res.ColorStateList;
 import android.preference.PreferenceManager;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -11,31 +12,40 @@ import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.core.graphics.ColorUtils;
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.helper.widget.Flow;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
 import androidx.databinding.DataBindingUtil;
 
-import com.wefika.flowlayout.FlowLayout;
-
-import java.util.List;
+import com.google.android.material.color.MaterialColors;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
 
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.ContactBinding;
+import eu.siacs.conversations.databinding.ItemContactBinding;
+import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.ListItem;
-import eu.siacs.conversations.ui.SettingsActivity;
+import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.utils.IrregularUnicodeDetector;
 import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.XEP0392Helper;
 import eu.siacs.conversations.xmpp.Jid;
 
+import java.util.List;
+
 public class ListItemAdapter extends ArrayAdapter<ListItem> {
 
 	protected XmppActivity activity;
 	private boolean showDynamicTags = false;
 	private OnTagClickedListener mOnTagClickedListener = null;
 	private final View.OnClickListener onTagTvClick = view -> {
-		if (view instanceof TextView && mOnTagClickedListener != null) {
-			TextView tv = (TextView) view;
+		if (view instanceof TextView tv && mOnTagClickedListener != null) {
 			final String tag = tv.getText().toString();
 			mOnTagClickedListener.onTagClicked(tag);
 		}
@@ -49,41 +59,84 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
 
 	public void refreshSettings() {
 		SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
-		this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, activity.getResources().getBoolean(R.bool.show_dynamic_tags));
+		this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, activity.getResources().getBoolean(R.bool.show_dynamic_tags));
 	}
 
+	@NonNull
 	@Override
-	public View getView(int position, View view, ViewGroup parent) {
+	public View getView(int position, View view, @NonNull ViewGroup parent) {
 		LayoutInflater inflater = activity.getLayoutInflater();
-		ListItem item = getItem(position);
+		final ListItem item = getItem(position);
 		ViewHolder viewHolder;
+		View innerView;
 		if (view == null) {
-			ContactBinding binding = DataBindingUtil.inflate(inflater,R.layout.contact,parent,false);
+			final ItemContactBinding binding = DataBindingUtil.inflate(inflater,R.layout.item_contact,parent,false);
 			viewHolder = ViewHolder.get(binding);
 			view = binding.getRoot();
+			innerView = binding.inner;
 		} else {
 			viewHolder = (ViewHolder) view.getTag();
+			innerView = view;
+		}
+		if (view.isActivated()) {
+			Log.d(Config.LOGTAG,"item "+item.getDisplayName()+" is activated");
 		}
-
 		if (activity.xmppConnectionService != null && activity.xmppConnectionService.getAccounts().size() > 1) {
-			viewHolder.inner.setBackgroundColor(item.getAccount().getColor(activity.isDarkTheme()));
+			innerView.setBackgroundColor(item.getAccount().getColor(activity.isDark()));
 		}
-
-		view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background));
-
-		List<ListItem.Tag> tags = item.getTags(activity);
-		if (tags.size() == 0 || !this.showDynamicTags) {
+		//view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background));
+		final List<ListItem.Tag> tags = item.getTags(activity);
+		final boolean hasMetaTags;
+		if (item instanceof Contact contact) {
+			hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
+		} else {
+			hasMetaTags = false;
+		}
+		if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
 			viewHolder.tags.setVisibility(View.GONE);
 		} else {
 			viewHolder.tags.setVisibility(View.VISIBLE);
-			viewHolder.tags.removeAllViewsInLayout();
-			for (ListItem.Tag tag : tags) {
-				TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.tags, false);
-				tv.setText(tag.getName());
-				tv.getBackground().mutate().setColorFilter(tag.getColor(), PorterDuff.Mode.SRC_IN);
+			viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1);
+			final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
+			for (final ListItem.Tag tag : tags) {
+				final String name = tag.getName();
+				final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.tags, false);
+				tv.setText(name);
+				tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(name))));
 				tv.setOnClickListener(this.onTagTvClick);
+				final int id = ViewCompat.generateViewId();
+				tv.setId(id);
+				viewIdBuilder.add(id);
 				viewHolder.tags.addView(tv);
 			}
+			if (item instanceof Contact contact) {
+				if (contact.isBlocked()) {
+					final TextView tv =
+							(TextView)
+									inflater.inflate(
+											R.layout.list_item_tag, viewHolder.tags, false);
+					tv.setText(R.string.blocked);
+					tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(),ContextCompat.getColor(tv.getContext(),R.color.gray_800))));
+					final int id = ViewCompat.generateViewId();
+					tv.setId(id);
+					viewIdBuilder.add(id);
+					viewHolder.tags.addView(tv);
+				} else {
+                    final Presence.Status status = contact.getShownStatus();
+                    if (status != Presence.Status.OFFLINE) {
+                        final TextView tv =
+                                (TextView)
+                                        inflater.inflate(
+                                                R.layout.list_item_tag, viewHolder.tags, false);
+						UIHelper.setStatus(tv, status);
+						final int id = ViewCompat.generateViewId();
+						tv.setId(id);
+						viewIdBuilder.add(id);
+						viewHolder.tags.addView(tv);
+                    }
+                }
+			}
+			viewHolder.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
 		}
 		final Jid jid = item.getJid();
 		if (jid != null) {
@@ -110,20 +163,22 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
 		private TextView name;
 		private TextView jid;
 		private ImageView avatar;
-		private FlowLayout tags;
 		private View inner;
+		private ConstraintLayout tags;
+		private Flow flowWidget;
 
 		private ViewHolder() {
 
 		}
 
-		public static ViewHolder get(ContactBinding binding) {
+		public static ViewHolder get(final ItemContactBinding binding) {
 			ViewHolder viewHolder = new ViewHolder();
 			viewHolder.name = binding.contactDisplayName;
 			viewHolder.jid = binding.contactJid;
 			viewHolder.avatar = binding.contactPhoto;
 			viewHolder.tags = binding.tags;
 			viewHolder.inner = binding.inner;
+			viewHolder.flowWidget = binding.flowWidget;
 			binding.getRoot().setTag(viewHolder);
 			return viewHolder;
 		}

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

@@ -1,47 +1,50 @@
 package eu.siacs.conversations.ui.adapter;
 
-import android.content.Context;
+import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 
-import androidx.annotation.AttrRes;
 import androidx.annotation.DimenRes;
+import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
+import androidx.core.widget.ImageViewCompat;
 import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.RecyclerView;
 
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
+import com.google.android.material.color.MaterialColors;
+import com.google.common.base.Strings;
 
-import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.MediaBinding;
+import eu.siacs.conversations.databinding.ItemMediaBinding;
 import eu.siacs.conversations.services.ExportBackupService;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.Attachment;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.ui.util.ViewUtil;
 
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
 public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHolder> {
 
-    public static final List<String> DOCUMENT_MIMES = Arrays.asList(
-            "application/pdf",
-            "application/vnd.oasis.opendocument.text",
-            "application/msword",
-            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-            "text/x-tex",
-            "text/plain"
-    );
+    public static final List<String> DOCUMENT_MIMES =
+            Arrays.asList(
+                    "application/pdf",
+                    "application/vnd.oasis.opendocument.text",
+                    "application/msword",
+                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+                    "text/x-tex",
+                    "text/plain");
+    public static final List<String> CODE_MIMES = Arrays.asList("text/html", "text/xml");
 
     private final ArrayList<Attachment> attachments = new ArrayList<>();
 
@@ -55,58 +58,77 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
     }
 
     @SuppressWarnings("rawtypes")
-    public static void setMediaSize(RecyclerView recyclerView, int mediaSize) {
+    public static void setMediaSize(final RecyclerView recyclerView, int mediaSize) {
         final RecyclerView.Adapter adapter = recyclerView.getAdapter();
-        if (adapter instanceof MediaAdapter) {
-            ((MediaAdapter) adapter).setMediaSize(mediaSize);
+        if (adapter instanceof MediaAdapter mediaAdapter) {
+            mediaAdapter.setMediaSize(mediaSize);
         }
     }
 
-    private static @AttrRes
-    int getImageAttr(Attachment attachment) {
-        final @AttrRes int attr;
+    public static @DrawableRes int getImageDrawable(final Attachment attachment) {
         if (attachment.getType() == Attachment.Type.LOCATION) {
-            attr = R.attr.media_preview_location;
+            return R.drawable.ic_location_pin_48dp;
         } else if (attachment.getType() == Attachment.Type.RECORDING) {
-            attr = R.attr.media_preview_recording;
+            return R.drawable.ic_mic_48dp;
         } else {
-            final String mime = attachment.getMime();
-            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"))) {
-                attr = R.attr.media_preview_calendar;
-            } else if (mime.equals("text/x-vcard")) {
-                attr = R.attr.media_preview_contact;
-            } else if (mime.equals("application/vnd.android.package-archive")) {
-                attr = R.attr.media_preview_app;
-            } else if (mime.equals("application/zip") || mime.equals("application/rar")) {
-                attr = R.attr.media_preview_archive;
-            } else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
-                attr = R.attr.media_preview_ebook;
-            } else if (mime.equals(ExportBackupService.MIME_TYPE)) {
-                attr = R.attr.media_preview_backup;
-            } else if (DOCUMENT_MIMES.contains(mime)) {
-                attr = R.attr.media_preview_document;
-            } else if (mime.equals("application/gpx+xml")) {
-                attr = R.attr.media_preview_tour;
-            } else if (mime.startsWith("image/")) {
-                attr = R.attr.media_preview_image;
-            } else {
-                attr = R.attr.media_preview_unknown;
-            }
+            return getImageDrawable(attachment.getMime());
+        }
+    }
+
+    private static @DrawableRes int getImageDrawable(final String mime) {
+
+        // TODO ideas for more mime types: XML, HTML documents, GPG/PGP files, eml files,
+        // spreadsheets (table symbol)
+
+        // add bz2 and tar.gz to archive detection
+
+        if (Strings.isNullOrEmpty(mime)) {
+            return R.drawable.ic_help_center_48dp;
+        } else if (mime.equals("audio/x-m4b")) {
+            return R.drawable.ic_play_lesson_48dp;
+        } else if (mime.startsWith("audio/")) {
+            return R.drawable.ic_headphones_48dp;
+        } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) {
+            return R.drawable.ic_event_48dp;
+        } else if (mime.equals("text/x-vcard")) {
+            return R.drawable.ic_person_48dp;
+        } else if (mime.equals("application/vnd.android.package-archive")) {
+            return R.drawable.ic_adb_48dp;
+        } else if (mime.equals("application/zip") || mime.equals("application/rar")) {
+            return R.drawable.ic_archive_48dp;
+        } else if (mime.equals("application/epub+zip")
+                || mime.equals("application/vnd.amazon.mobi8-ebook")) {
+            return R.drawable.ic_book_48dp;
+        } else if (mime.equals(ExportBackupService.MIME_TYPE)) {
+            return R.drawable.ic_backup_48dp;
+        } else if (DOCUMENT_MIMES.contains(mime)) {
+            return R.drawable.ic_description_48dp;
+        } else if (mime.equals("application/gpx+xml")) {
+            return R.drawable.ic_tour_48dp;
+        } else if (mime.startsWith("image/")) {
+            return R.drawable.ic_image_48dp;
+        } else if (mime.startsWith("video/")) {
+            return R.drawable.ic_movie_48dp;
+        } else if (CODE_MIMES.contains(mime)) {
+            return R.drawable.ic_code_48dp;
+        } else if (mime.equals("message/rfc822")) {
+            return R.drawable.ic_email_48dp;
+        } else {
+            return R.drawable.ic_help_center_48dp;
         }
-        return attr;
     }
 
-    static void renderPreview(Context context, Attachment attachment, ImageView imageView) {
-        imageView.setBackgroundColor(StyledAttributes.getColor(context, R.attr.color_background_tertiary));
-        imageView.setImageAlpha(Math.round(StyledAttributes.getFloat(context, R.attr.icon_alpha) * 255));
-        imageView.setImageDrawable(StyledAttributes.getDrawable(context, getImageAttr(attachment)));
+    static void renderPreview(final Attachment attachment, final ImageView imageView) {
+        ImageViewCompat.setImageTintList(
+                imageView,
+                ColorStateList.valueOf(
+                        MaterialColors.getColor(
+                                imageView, com.google.android.material.R.attr.colorOnSurface)));
+        imageView.setImageResource(getImageDrawable(attachment));
+        imageView.setBackgroundColor(
+                MaterialColors.getColor(
+                        imageView,
+                        com.google.android.material.R.attr.colorSurfaceContainerHighest));
     }
 
     private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) {
@@ -126,8 +148,7 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
     private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
         if (imageView != null) {
             final Drawable drawable = imageView.getDrawable();
-            if (drawable instanceof AsyncDrawable) {
-                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+            if (drawable instanceof AsyncDrawable asyncDrawable) {
                 return asyncDrawable.getBitmapWorkerTask();
             }
         }
@@ -137,8 +158,9 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
     @NonNull
     @Override
     public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
-        MediaBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media, parent, false);
+        final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+        ItemMediaBinding binding =
+                DataBindingUtil.inflate(layoutInflater, R.layout.item_media, parent, false);
         return new MediaViewHolder(binding);
     }
 
@@ -146,16 +168,15 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
     public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
         final Attachment attachment = attachments.get(position);
         if (attachment.renderThumbnail()) {
-            holder.binding.media.setImageAlpha(255);
             loadPreview(attachment, holder.binding.media);
         } else {
             cancelPotentialWork(attachment, holder.binding.media);
-            renderPreview(activity, attachment, holder.binding.media);
+            renderPreview(attachment, holder.binding.media);
         }
         holder.binding.getRoot().setOnClickListener(v -> ViewUtil.view(activity, attachment));
     }
 
-    public void setAttachments(List<Attachment> attachments) {
+    public void setAttachments(final List<Attachment> attachments) {
         this.attachments.clear();
         this.attachments.addAll(attachments);
         notifyDataSetChanged();
@@ -167,16 +188,21 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
 
     private void loadPreview(Attachment attachment, ImageView imageView) {
         if (cancelPotentialWork(attachment, imageView)) {
-            final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment, mediaSize, true);
+            final Bitmap bm =
+                    activity.xmppConnectionService
+                            .getFileBackend()
+                            .getPreviewForUri(attachment, mediaSize, true);
             if (bm != null) {
                 cancelPotentialWork(attachment, imageView);
                 imageView.setImageBitmap(bm);
-                imageView.setBackgroundColor(0x00000000);
+                imageView.setBackgroundColor(Color.TRANSPARENT);
             } else {
+                // TODO consider if this is still a good, general purpose loading color
                 imageView.setBackgroundColor(0xff333333);
                 imageView.setImageDrawable(null);
                 final BitmapWorkerTask task = new BitmapWorkerTask(mediaSize, imageView);
-                final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+                final AsyncDrawable asyncDrawable =
+                        new AsyncDrawable(activity.getResources(), null, task);
                 imageView.setImageDrawable(asyncDrawable);
                 try {
                     task.execute(attachment);
@@ -204,11 +230,11 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
         }
     }
 
-    class MediaViewHolder extends RecyclerView.ViewHolder {
+    static class MediaViewHolder extends RecyclerView.ViewHolder {
 
-        private final MediaBinding binding;
+        private final ItemMediaBinding binding;
 
-        MediaViewHolder(MediaBinding binding) {
+        MediaViewHolder(ItemMediaBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
         }
@@ -225,13 +251,15 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
         }
 
         @Override
-        protected Bitmap doInBackground(Attachment... params) {
+        protected Bitmap doInBackground(final Attachment... params) {
             this.attachment = params[0];
             final XmppActivity activity = XmppActivity.find(imageViewReference);
             if (activity == null) {
                 return null;
             }
-            return activity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, mediaSize, false);
+            return activity.xmppConnectionService
+                    .getFileBackend()
+                    .getPreviewForUri(this.attachment, mediaSize, false);
         }
 
         @Override

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

@@ -9,28 +9,35 @@ import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.core.widget.ImageViewCompat;
 import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.RecyclerView;
 
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
-
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.MediaPreviewBinding;
+import eu.siacs.conversations.databinding.ItemMediaPreviewBinding;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.ConversationFragment;
+import eu.siacs.conversations.ui.ShowLocationActivity;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.Attachment;
 
-public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapter.MediaPreviewViewHolder> {
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.RejectedExecutionException;
+
+public class MediaPreviewAdapter
+        extends RecyclerView.Adapter<MediaPreviewAdapter.MediaPreviewViewHolder> {
 
     private final ArrayList<Attachment> mediaPreviews = new ArrayList<>();
 
@@ -43,8 +50,9 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
     @NonNull
     @Override
     public MediaPreviewViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
-        MediaPreviewBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media_preview, parent, false);
+        final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+        ItemMediaPreviewBinding binding =
+                DataBindingUtil.inflate(layoutInflater, R.layout.item_media_preview, parent, false);
         return new MediaPreviewViewHolder(binding);
     }
 
@@ -53,32 +61,43 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
         final Context context = conversationFragment.getActivity();
         final Attachment attachment = mediaPreviews.get(position);
         if (attachment.renderThumbnail()) {
-            holder.binding.mediaPreview.setImageAlpha(255);
+            ImageViewCompat.setImageTintList(holder.binding.mediaPreview, null);
             loadPreview(attachment, holder.binding.mediaPreview);
         } else {
             cancelPotentialWork(attachment, holder.binding.mediaPreview);
-            MediaAdapter.renderPreview(context, attachment, holder.binding.mediaPreview);
+            MediaAdapter.renderPreview(attachment, holder.binding.mediaPreview);
         }
-        holder.binding.deleteButton.setOnClickListener(v -> {
-            final int pos = mediaPreviews.indexOf(attachment);
-            mediaPreviews.remove(pos);
-            notifyItemRemoved(pos);
-            conversationFragment.toggleInputMethod();
-        });
+        holder.binding.deleteButton.setOnClickListener(
+                v -> {
+                    final int pos = mediaPreviews.indexOf(attachment);
+                    mediaPreviews.remove(pos);
+                    notifyItemRemoved(pos);
+                    conversationFragment.toggleInputMethod();
+                });
         holder.binding.mediaPreview.setOnClickListener(v -> view(context, attachment));
     }
 
-    private static void view(final Context context, Attachment attachment) {
+    private static void view(final Context context, final Attachment attachment) {
         final Intent view = new Intent(Intent.ACTION_VIEW);
-        final Uri uri = FileBackend.getUriForUri(context, attachment.getUri());
-        view.setDataAndType(uri, attachment.getMime());
-        view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        if (attachment.getType() == Attachment.Type.LOCATION) {
+            view.setClass(context, ShowLocationActivity.class);
+            view.setData(attachment.getUri());
+        } else {
+            final Uri uri = FileBackend.getUriForUri(context, attachment.getUri());
+            view.setDataAndType(uri, attachment.getMime());
+            view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        }
         try {
             context.startActivity(view);
         } catch (final ActivityNotFoundException e) {
-            Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
+            Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT)
+                    .show();
         } catch (final SecurityException e) {
-            Toast.makeText(context, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show();
+            Toast.makeText(
+                            context,
+                            R.string.sharing_application_not_grant_permission,
+                            Toast.LENGTH_SHORT)
+                    .show();
         }
     }
 
@@ -90,16 +109,27 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
     private void loadPreview(Attachment attachment, ImageView imageView) {
         if (cancelPotentialWork(attachment, imageView)) {
             XmppActivity activity = (XmppActivity) conversationFragment.getActivity();
-            final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment,Math.round(activity.getResources().getDimension(R.dimen.media_preview_size)),true);
+            final Bitmap bm =
+                    activity.xmppConnectionService
+                            .getFileBackend()
+                            .getPreviewForUri(
+                                    attachment,
+                                    Math.round(
+                                            activity.getResources()
+                                                    .getDimension(R.dimen.media_preview_size)),
+                                    true);
             if (bm != null) {
                 cancelPotentialWork(attachment, imageView);
                 imageView.setImageBitmap(bm);
                 imageView.setBackgroundColor(0x00000000);
             } else {
-                imageView.setBackgroundColor(0xff333333);
+                imageView.setBackgroundColor(
+                        ContextCompat.getColor(imageView.getContext(), R.color.gray_800));
                 imageView.setImageDrawable(null);
                 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
-                final AsyncDrawable asyncDrawable = new AsyncDrawable(conversationFragment.getActivity().getResources(), null, task);
+                final AsyncDrawable asyncDrawable =
+                        new AsyncDrawable(
+                                conversationFragment.getActivity().getResources(), null, task);
                 imageView.setImageDrawable(asyncDrawable);
                 try {
                     task.execute(attachment);
@@ -126,8 +156,7 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
     private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
         if (imageView != null) {
             final Drawable drawable = imageView.getDrawable();
-            if (drawable instanceof AsyncDrawable) {
-                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+            if (drawable instanceof AsyncDrawable asyncDrawable) {
                 return asyncDrawable.getBitmapWorkerTask();
             }
         }
@@ -140,7 +169,7 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
     }
 
     public boolean hasAttachments() {
-        return mediaPreviews.size() > 0;
+        return !mediaPreviews.isEmpty();
     }
 
     public ArrayList<Attachment> getAttachments() {
@@ -153,9 +182,9 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
 
     static class MediaPreviewViewHolder extends RecyclerView.ViewHolder {
 
-        private final MediaPreviewBinding binding;
+        private final ItemMediaPreviewBinding binding;
 
-        MediaPreviewViewHolder(MediaPreviewBinding binding) {
+        MediaPreviewViewHolder(ItemMediaPreviewBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
         }
@@ -189,7 +218,14 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
             if (activity == null) {
                 return null;
             }
-            return activity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, Math.round(activity.getResources().getDimension(R.dimen.media_preview_size)), false);
+            return activity.xmppConnectionService
+                    .getFileBackend()
+                    .getPreviewForUri(
+                            this.attachment,
+                            Math.round(
+                                    activity.getResources()
+                                            .getDimension(R.dimen.media_preview_size)),
+                            false);
         }
 
         @Override

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

@@ -3,10 +3,10 @@ package eu.siacs.conversations.ui.adapter;
 import android.Manifest;
 import android.app.Activity;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.content.res.ColorStateList;
 import android.graphics.Typeface;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -31,7 +31,6 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.ArrayAdapter;
-import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ListAdapter;
@@ -40,6 +39,11 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.annotation.AttrRes;
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
@@ -49,8 +53,13 @@ import com.cheogram.android.MessageTextActionModeCallback;
 import com.cheogram.android.SwipeDetector;
 import com.cheogram.android.WebxdcPage;
 import com.cheogram.android.WebxdcUpdate;
+import androidx.core.widget.ImageViewCompat;
 
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.color.MaterialColors;
+import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 
 import com.lelloman.identicon.view.GithubIdenticonView;
 
@@ -69,6 +78,7 @@ import io.ipfs.cid.Cid;
 
 import me.saket.bettermovementmethod.BetterLinkMovementMethod;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -86,18 +96,18 @@ import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.NotificationService;
+import eu.siacs.conversations.ui.Activities;
 import eu.siacs.conversations.ui.ConversationFragment;
 import eu.siacs.conversations.ui.ConversationsActivity;
-import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.service.AudioPlayer;
 import eu.siacs.conversations.ui.text.DividerSpan;
 import eu.siacs.conversations.ui.text.QuoteSpan;
+import eu.siacs.conversations.ui.util.Attachment;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.ui.util.MyLinkify;
 import eu.siacs.conversations.ui.util.QuoteHelper;
 import eu.siacs.conversations.ui.util.ShareUtil;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.ui.util.ViewUtil;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.Emoticons;
@@ -110,6 +120,12 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.mam.MamReference;
 import eu.siacs.conversations.xml.Element;
 
+import java.net.URI;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 public class MessageAdapter extends ArrayAdapter<Message> {
 
     public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
@@ -129,11 +145,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
     private OnInlineImageLongClicked mOnInlineImageLongClickedListener;
     private boolean mUseGreenBackground = false;
+    private BubbleDesign bubbleDesign = new BubbleDesign(false, false);
     private final boolean mForceNames;
     private final Map<String, WebxdcUpdate> lastWebxdcUpdate = new HashMap<>();
     private String selectionUuid = null;
 
-    public MessageAdapter(final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
+    public MessageAdapter(
+            final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
         super(activity, 0, messages);
         this.audioPlayer = new AudioPlayer(this);
         this.activity = activity;
@@ -192,8 +210,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         return activity;
     }
 
-    public void setOnContactPictureLongClicked(
-            OnContactPictureLongClicked listener) {
+    public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) {
         this.mOnContactPictureLongClickedListener = listener;
     }
 
@@ -227,207 +244,221 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         return this.getItemViewType(getItem(position));
     }
 
-    private int getMessageTextColor(boolean onDark, boolean primary) {
-        if (onDark) {
-            return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70);
-        } else {
-            return ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54);
-        }
-    }
-
-    private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) {
-        String filesize = null;
-        String info = null;
-        boolean error = false;
+    private void displayStatus(
+            final ViewHolder viewHolder,
+            final Message message,
+            final int type,
+            final BubbleColor bubbleColor) {
+        final int mergedStatus = message.getMergedStatus();
+        final boolean error;
         if (viewHolder.indicatorReceived != null) {
             viewHolder.indicatorReceived.setVisibility(View.GONE);
         }
-
-        if (viewHolder.edit_indicator != null) {
-            if (message.edited() && message.getModerated() == null) {
-                viewHolder.edit_indicator.setVisibility(View.VISIBLE);
-                viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp);
-                viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f);
-            } else {
-                viewHolder.edit_indicator.setVisibility(View.GONE);
-            }
-        }
         final Transferable transferable = message.getTransferable();
-        boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
-                && message.getMergedStatus() <= Message.STATUS_RECEIVED;
-        if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) {
-            FileParams params = message.getFileParams();
-            filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
-            if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) {
+        final boolean multiReceived =
+                message.getConversation().getMode() == Conversation.MODE_MULTI
+                        && mergedStatus <= Message.STATUS_RECEIVED;
+        final String fileSize;
+        if (message.isFileOrImage()
+                || transferable != null
+                || MessageUtils.unInitiatedButKnownSize(message)) {
+            final FileParams params = message.getFileParams();
+            fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
+            if (message.getStatus() == Message.STATUS_SEND_FAILED
+                    || (transferable != null
+                            && (transferable.getStatus() == Transferable.STATUS_FAILED
+                                    || transferable.getStatus()
+                                            == Transferable.STATUS_CANCELLED))) {
                 error = true;
+            } else {
+                error = message.getStatus() == Message.STATUS_SEND_FAILED;
             }
+        } else {
+            fileSize = null;
+            error = message.getStatus() == Message.STATUS_SEND_FAILED;
         }
-        switch (message.getMergedStatus()) {
-            case Message.STATUS_WAITING:
-                info = getContext().getString(R.string.waiting);
-                break;
-            case Message.STATUS_UNSEND:
-                if (transferable != null) {
-                    info = getContext().getString(R.string.sending_file, transferable.getProgress());
-                } else {
-                    info = getContext().getString(R.string.sending);
-                }
-                break;
-            case Message.STATUS_OFFERED:
-                info = getContext().getString(R.string.offering);
-                break;
-            case Message.STATUS_SEND_RECEIVED:
-            case Message.STATUS_SEND_DISPLAYED:
-                if (viewHolder.indicatorReceived != null) {
-                    viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_done_white_18dp : R.drawable.ic_done_black_18dp);
-                    viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f);
-                    viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
-                }
-                break;
-            case Message.STATUS_SEND_FAILED:
-                final String errorMessage = message.getErrorMessage();
-                if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
-                    info = getContext().getString(R.string.cancelled);
-                } else if (errorMessage != null) {
-                    final String[] errorParts = errorMessage.split("\\u001f", 2);
-                    if (errorParts.length == 2) {
-                        switch (errorParts[0]) {
-                            case "file-too-large":
-                                info = getContext().getString(R.string.file_too_large);
-                                break;
-                            default:
-                                info = getContext().getString(R.string.send_failed);
-                                break;
-                        }
-                    } else {
-                        info = getContext().getString(R.string.send_failed);
-                    }
+        if (type == SENT) {
+            final @DrawableRes Integer receivedIndicator =
+                    getMessageStatusAsDrawable(message, mergedStatus);
+            if (receivedIndicator == null) {
+                viewHolder.indicatorReceived.setVisibility(View.INVISIBLE);
+            } else {
+                viewHolder.indicatorReceived.setImageResource(receivedIndicator);
+                if (mergedStatus == Message.STATUS_SEND_FAILED) {
+                    setImageTintError(viewHolder.indicatorReceived);
                 } else {
-                    info = getContext().getString(R.string.send_failed);
+                    setImageTint(viewHolder.indicatorReceived, bubbleColor);
                 }
-                error = true;
-                break;
-            default:
-                if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) {
-                    info = UIHelper.getMessageDisplayName(message).replace(' ', '\u00A0');
-                }
-                break;
+                viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+            }
         }
+        final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus);
+
         if (error && type == SENT) {
-            if (darkBackground) {
-                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark);
-            } else {
-                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning);
-            }
+            viewHolder.time.setTextColor(
+                    MaterialColors.getColor(
+                            viewHolder.time, com.google.android.material.R.attr.colorError));
         } else {
-            if (darkBackground) {
-                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
-            } else {
-                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption);
-            }
-            viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground, false));
+            setTextColor(viewHolder.time, bubbleColor);
         }
         if (message.getEncryption() == Message.ENCRYPTION_NONE) {
             viewHolder.indicator.setVisibility(View.GONE);
         } else {
             boolean verified = false;
             if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
-                final FingerprintStatus status = message.getConversation()
-                        .getAccount().getAxolotlService().getFingerprintTrust(
-                                message.getFingerprint());
+                final FingerprintStatus status =
+                        message.getConversation()
+                                .getAccount()
+                                .getAxolotlService()
+                                .getFingerprintTrust(message.getFingerprint());
                 if (status != null && status.isVerified()) {
                     verified = true;
                 }
             }
             if (verified) {
-                viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp);
+                viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp);
             } else {
-                viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
+                viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp);
             }
-            if (darkBackground) {
-                viewHolder.indicator.setAlpha(0.7f);
+            if (error && type == SENT) {
+                setImageTintError(viewHolder.indicator);
             } else {
-                viewHolder.indicator.setAlpha(0.57f);
+                setImageTint(viewHolder.indicator, bubbleColor);
             }
             viewHolder.indicator.setVisibility(View.VISIBLE);
         }
 
-        final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
+        if (viewHolder.edit_indicator != null) {
+            if (message.edited()) {
+                viewHolder.edit_indicator.setVisibility(View.VISIBLE);
+                if (error && type == SENT) {
+                    setImageTintError(viewHolder.edit_indicator);
+                } else {
+                    setImageTint(viewHolder.edit_indicator, bubbleColor);
+                }
+            } else {
+                viewHolder.edit_indicator.setVisibility(View.GONE);
+            }
+        }
+
+        final String formattedTime =
+                UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
         final String bodyLanguage = message.getBodyLanguage();
-        final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format("\u00A0\u00B7 %s", bodyLanguage.toUpperCase(Locale.US));
+        final ImmutableList.Builder<String> timeInfoBuilder = new ImmutableList.Builder<>();
         if (message.getStatus() <= Message.STATUS_RECEIVED) {
-            if ((filesize != null) && (info != null)) {
-                viewHolder.time.setText(formattedTime + "\u00A0\u00B7" + filesize + "\u00A0\u00B7 " + info + bodyLanguageInfo);
-            } else if ((filesize == null) && (info != null)) {
-                viewHolder.time.setText(formattedTime + "\u00A0\u00B7 " + info + bodyLanguageInfo);
-            } else if ((filesize != null) && (info == null)) {
-                viewHolder.time.setText(formattedTime + "\u00A0\u00B7 " + filesize + bodyLanguageInfo);
-            } else {
-                viewHolder.time.setText(formattedTime + bodyLanguageInfo);
+            timeInfoBuilder.add(formattedTime);
+            if (fileSize != null) {
+                timeInfoBuilder.add(fileSize);
             }
-        } else {
-            if ((filesize != null) && (info != null)) {
-                viewHolder.time.setText(filesize + "\u00A0\u00B7 " + info + bodyLanguageInfo);
-            } else if ((filesize == null) && (info != null)) {
-                if (error) {
-                    viewHolder.time.setText(info + "\u00A0\u00B7 " + formattedTime + bodyLanguageInfo);
-                } else {
-                    viewHolder.time.setText(info);
+            if (mForceNames || multiReceived) {
+                final String displayName = UIHelper.getMessageDisplayName(message);
+                if (displayName != null) {
+                    timeInfoBuilder.add(displayName);
                 }
-            } else if ((filesize != null) && (info == null)) {
-                viewHolder.time.setText(filesize + "\u00A0\u00B7 " + formattedTime + bodyLanguageInfo);
+            }
+            if (bodyLanguage != null) {
+                timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
+            }
+        } else {
+            if (bodyLanguage != null) {
+                timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
+            }
+            if (fileSize != null) {
+                timeInfoBuilder.add(fileSize);
+            }
+            // for space reasons we display only 'additional status info' (send progress or concrete
+            // failure reason) or the time
+            if (additionalStatusInfo != null) {
+                timeInfoBuilder.add(additionalStatusInfo);
             } else {
-                viewHolder.time.setText(formattedTime + bodyLanguageInfo);
+                timeInfoBuilder.add(formattedTime);
             }
         }
+        final var timeInfo = timeInfoBuilder.build();
+        viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo));
+    }
+
+    public static @DrawableRes Integer getMessageStatusAsDrawable(
+            final Message message, final int status) {
+        final var transferable = message.getTransferable();
+        return switch (status) {
+            case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp;
+            case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp;
+            case Message.STATUS_SEND -> R.drawable.ic_done_24dp;
+            case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable
+                    .ic_done_all_24dp;
+            case Message.STATUS_SEND_FAILED -> {
+                final String errorMessage = message.getErrorMessage();
+                if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
+                    yield R.drawable.ic_cancel_24dp;
+                } else {
+                    yield R.drawable.ic_error_24dp;
+                }
+            }
+            case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp;
+            default -> null;
+        };
     }
 
-    private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground, final Message message, int type) {
-        displayDownloadableMessage(viewHolder, message, "", darkBackground, type);
-        int imageVisibility = viewHolder.image.getVisibility();
-        displayInfoMessage(viewHolder, text, darkBackground);
-        viewHolder.image.setVisibility(imageVisibility);
+    @Nullable
+    private String getAdditionalStatusInfo(final Message message, final int mergedStatus) {
+        final String additionalStatusInfo;
+        if (mergedStatus == Message.STATUS_SEND_FAILED) {
+            final String errorMessage = Strings.nullToEmpty(message.getErrorMessage());
+            final String[] errorParts = errorMessage.split("\\u001f", 2);
+            if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) {
+                additionalStatusInfo = getContext().getString(R.string.file_too_large);
+            } else {
+                additionalStatusInfo = null;
+            }
+        } else if (mergedStatus == Message.STATUS_UNSEND) {
+            final var transferable = message.getTransferable();
+            if (transferable == null) {
+                return null;
+            }
+            return getContext().getString(R.string.sending_file, transferable.getProgress());
+        } else {
+            additionalStatusInfo = null;
+        }
+        return additionalStatusInfo;
     }
 
-    private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) {
+    private void displayInfoMessage(
+            ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) {
         viewHolder.download_button.setVisibility(View.GONE);
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.image.setVisibility(View.GONE);
         viewHolder.messageBody.setVisibility(View.VISIBLE);
         viewHolder.messageBody.setText(text);
-        if (darkBackground) {
-            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark);
-        } else {
-            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary);
-        }
+        viewHolder.messageBody.setTextColor(
+                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor));
         viewHolder.messageBody.setTextIsSelectable(false);
     }
 
-    private void displayEmojiMessage(final ViewHolder viewHolder, final SpannableStringBuilder body, final boolean darkBackground) {
+    private void displayEmojiMessage(
+            final ViewHolder viewHolder, final SpannableStringBuilder body, final BubbleColor bubbleColor) {
         viewHolder.download_button.setVisibility(View.GONE);
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.image.setVisibility(View.GONE);
         viewHolder.messageBody.setVisibility(View.VISIBLE);
-        if (darkBackground) {
-            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark);
-        } else {
-            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji);
-        }
+        setTextColor(viewHolder.messageBody, bubbleColor);
         ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class);
         float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f;
-        body.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        body.setSpan(
+                new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
         viewHolder.messageBody.setText(body);
     }
 
-    private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
+    private void applyQuoteSpan(
+            final TextView textView,
+            SpannableStringBuilder body,
+            int start,
+            int end,
+            final BubbleColor bubbleColor) {
         if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
             body.insert(start++, "\n");
             body.setSpan(
-                new DividerSpan(false),
-                start - ("\n".equals(body.subSequence(start - 2, start - 1).toString()) ? 2 : 1),
-                start,
-                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
-            );
+                    new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
             end++;
         }
         if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
@@ -439,17 +470,28 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
             );
         }
-        int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
-                : ContextCompat.getColor(activity, R.color.green700_desaturated);
-        DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
-        body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+        body.setSpan(
+                new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics),
+                start,
+                end,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+    }
+
+    public boolean handleTextQuotes(final TextView textView, final SpannableStringBuilder body) {
+        final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
+        final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
+        return handleTextQuotes(textView, body, bubbleColor);
     }
 
     /**
-     * Applies QuoteSpan to group of lines which starts with > or » characters.
-     * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
+     * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks
+     * and applies DividerSpan to them to show a padding between quote and text.
      */
-    public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
+    public boolean handleTextQuotes(
+            final TextView textView,
+            final SpannableStringBuilder body,
+            final BubbleColor bubbleColor) {
         boolean startsWithQuote = false;
         int quoteDepth = 1;
         while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
@@ -468,7 +510,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                             if (i == 0) startsWithQuote = true;
                         } else if (quoteStart >= 0) {
                             // Line start without quote, apply spans there
-                            applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
+                            applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor);
                             quoteStart = -1;
                         }
                     }
@@ -493,7 +535,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             }
             if (quoteStart >= 0) {
                 // Apply spans to finishing open quote
-                applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
+                applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor);
             }
             quoteDepth++;
         }
@@ -501,7 +543,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     }
 
     private SpannableStringBuilder getSpannableBody(final Message message) {
-        Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo), null);
+        Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null);
         return message.getMergedBody((cid) -> {
             try {
                 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
@@ -525,19 +567,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         }, fallbackImg);
     }
 
-    private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
+    private void displayTextMessage(
+            final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
         viewHolder.download_button.setVisibility(View.GONE);
         viewHolder.image.setVisibility(View.GONE);
         viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.messageBody.setVisibility(View.GONE);
+        viewHolder.messageBody.setVisibility(View.VISIBLE);
+        setTextColor(viewHolder.messageBody, bubbleColor);
+        setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont);
 
-        if (darkBackground) {
-            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark);
-        } else {
-            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1);
-        }
-        viewHolder.messageBody.setHighlightColor(ContextCompat.getColor(activity, darkBackground
-                ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
         viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
 
         if (message.getBody() != null && !message.getBody().equals("")) {
@@ -553,22 +591,20 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                 body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
                 body.append("\u2026");
             }
-            Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
+            Message.MergeSeparator[] mergeSeparators =
+                    body.getSpans(0, body.length(), Message.MergeSeparator.class);
             for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
                 int start = body.getSpanStart(mergeSeparator);
                 int end = body.getSpanEnd(mergeSeparator);
                 body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
             }
-            for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) {
-                int start = body.getSpanStart(quote);
-                int end = body.getSpanEnd(quote);
-                body.removeSpan(quote);
-                applyQuoteSpan(body, start, end, darkBackground);
-            }
-            boolean startsWithQuote = handleTextQuotes(body, darkBackground);
+            boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor);
             if (!message.isPrivateMessage()) {
                 if (hasMeCommand) {
-                    body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
+                    body.setSpan(
+                            new StyleSpan(Typeface.BOLD_ITALIC),
+                            0,
+                            nick.length(),
                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                 }
             } else {
@@ -577,31 +613,55 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     privateMarker = activity.getString(R.string.private_message);
                 } else {
                     Jid cp = message.getCounterpart();
-                    privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
+                    privateMarker =
+                            activity.getString(
+                                    R.string.private_message_to,
+                                    Strings.nullToEmpty(cp == null ? null : cp.getResource()));
                 }
                 body.insert(0, privateMarker);
                 int privateMarkerIndex = privateMarker.length();
                 if (startsWithQuote) {
                     body.insert(privateMarkerIndex, "\n\n");
-                    body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2,
+                    body.setSpan(
+                            new DividerSpan(false),
+                            privateMarkerIndex,
+                            privateMarkerIndex + 2,
                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                 } else {
                     body.insert(privateMarkerIndex, " ");
                 }
-                body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-                body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                body.setSpan(
+                        new ForegroundColorSpan(
+                                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
+                        0,
+                        privateMarkerIndex,
+                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                body.setSpan(
+                        new StyleSpan(Typeface.BOLD),
+                        0,
+                        privateMarkerIndex,
+                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                 if (hasMeCommand) {
-                    body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1,
-                            privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    body.setSpan(
+                            new StyleSpan(Typeface.BOLD_ITALIC),
+                            privateMarkerIndex + 1,
+                            privateMarkerIndex + 1 + nick.length(),
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                 }
             }
-            if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) {
-                if (message.getConversation() instanceof Conversation) {
-                    final Conversation conversation = (Conversation) message.getConversation();
-                    Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick());
+            if (message.getConversation().getMode() == Conversation.MODE_MULTI
+                    && message.getStatus() == Message.STATUS_RECEIVED) {
+                if (message.getConversation() instanceof Conversation conversation) {
+                    Pattern pattern =
+                            NotificationService.generateNickHighlightPattern(
+                                    conversation.getMucOptions().getActualNick());
                     Matcher matcher = pattern.matcher(body);
                     while (matcher.find()) {
-                        body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                        body.setSpan(
+                                new StyleSpan(Typeface.BOLD),
+                                matcher.start(),
+                                matcher.end(),
+                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                     }
 
                     pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName());
@@ -614,13 +674,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
             while (matcher.find()) {
                 if (matcher.start() < matcher.end()) {
-                    body.setSpan(new RelativeSizeSpan(1.2f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    body.setSpan(
+                            new RelativeSizeSpan(1.2f),
+                            matcher.start(),
+                            matcher.end(),
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                 }
             }
 
             StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
             if (highlightedTerm != null) {
-                StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody));
+                StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
             }
             MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
             viewHolder.messageBody.setAutoLinkMask(0);
@@ -655,12 +719,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         } else {
             viewHolder.messageBody.setText("");
             viewHolder.messageBody.setTextIsSelectable(false);
-            toggleWhisperInfo(viewHolder, message, darkBackground);
+            toggleWhisperInfo(viewHolder, message, bubbleColor);
         }
     }
 
-    private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground, final int type) {
-        displayTextMessage(viewHolder, message, darkBackground, type);
+    private void displayDownloadableMessage(
+            ViewHolder viewHolder,
+            final Message message,
+            String text,
+            final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        toggleWhisperInfo(viewHolder, message, bubbleColor);
         viewHolder.image.setVisibility(View.GONE);
         List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
         if (thumbs != null && !thumbs.isEmpty()) {
@@ -705,13 +774,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.download_button.setVisibility(View.VISIBLE);
         viewHolder.download_button.setText(text);
-        viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
+        final var attachment = Attachment.of(message);
+        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
+        viewHolder.download_button.setIconResource(imageResource);
+        viewHolder.download_button.setOnClickListener(
+                v -> ConversationFragment.downloadFile(activity, message));
     }
 
-    private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
+    private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
         Cid webxdcCid = message.getFileParams().getCids().get(0);
         WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
-        displayTextMessage(viewHolder, message, darkBackground, type);
+        displayTextMessage(viewHolder, message, bubbleColor, type);
         viewHolder.image.setVisibility(View.GONE);
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.download_button.setVisibility(View.VISIBLE);
@@ -759,36 +832,53 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         }
     }
 
-    private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
-        displayTextMessage(viewHolder, message, darkBackground, type);
+    private void displayOpenableMessage(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        toggleWhisperInfo(viewHolder, message, bubbleColor);
         viewHolder.image.setVisibility(View.GONE);
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.download_button.setVisibility(View.VISIBLE);
-        viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message)));
+        viewHolder.download_button.setText(
+                activity.getString(
+                        R.string.open_x_file,
+                        UIHelper.getFileDescriptionString(activity, message)));
+        final var attachment = Attachment.of(message);
+        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
+        viewHolder.download_button.setIconResource(imageResource);
         viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
     }
 
-    private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
-        displayTextMessage(viewHolder, message, darkBackground, type);
+    private void displayLocationMessage(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        toggleWhisperInfo(viewHolder, message, bubbleColor);
         viewHolder.image.setVisibility(View.GONE);
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.download_button.setVisibility(View.VISIBLE);
         viewHolder.download_button.setText(R.string.show_location);
+        final var attachment = Attachment.of(message);
+        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
+        viewHolder.download_button.setIconResource(imageResource);
         viewHolder.download_button.setOnClickListener(v -> showLocation(message));
     }
 
-    private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground, final int type) {
-        displayTextMessage(viewHolder, message, darkBackground, type);
+    private void displayAudioMessage(
+            ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        toggleWhisperInfo(viewHolder, message, bubbleColor);
         viewHolder.image.setVisibility(View.GONE);
         viewHolder.download_button.setVisibility(View.GONE);
         final RelativeLayout audioPlayer = viewHolder.audioPlayer;
         audioPlayer.setVisibility(View.VISIBLE);
-        AudioPlayer.ViewHolder.get(audioPlayer).setDarkBackground(darkBackground);
+        AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
         this.audioPlayer.init(audioPlayer, message);
     }
 
-    private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
-        displayTextMessage(viewHolder, message, darkBackground, type);
+    private void displayMediaPreviewMessage(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        toggleWhisperInfo(viewHolder, message, bubbleColor);
         viewHolder.download_button.setVisibility(View.GONE);
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.image.setVisibility(View.VISIBLE);
@@ -815,24 +905,38 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             scaledW = (int) target;
             scaledH = (int) (h / ((double) w / target));
         }
-        final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH);
+        final LinearLayout.LayoutParams layoutParams =
+                new LinearLayout.LayoutParams(scaledW, scaledH);
         layoutParams.setMargins(0, topMargin ? (int) (metrics.density * 4) : 0, 0, (int) (metrics.density * 4));
         layoutParams.gravity = Gravity.CENTER;
         image.setLayoutParams(layoutParams);
     }
 
-    private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) {
+    private void toggleWhisperInfo(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
         if (message.isPrivateMessage()) {
             final String privateMarker;
             if (message.getStatus() <= Message.STATUS_RECEIVED) {
                 privateMarker = activity.getString(R.string.private_message);
             } else {
                 Jid cp = message.getCounterpart();
-                privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
+                privateMarker =
+                        activity.getString(
+                                R.string.private_message_to,
+                                Strings.nullToEmpty(cp == null ? null : cp.getResource()));
             }
             final SpannableString body = new SpannableString(privateMarker);
-            body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-            body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            body.setSpan(
+                    new ForegroundColorSpan(
+                            bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
+                    0,
+                    privateMarker.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            body.setSpan(
+                    new StyleSpan(Typeface.BOLD),
+                    0,
+                    privateMarker.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
             viewHolder.messageBody.setText(body);
             viewHolder.messageBody.setVisibility(View.VISIBLE);
         } else {
@@ -850,19 +954,28 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             timestamp = System.currentTimeMillis();
         }
         conversation.messagesLoaded.set(true);
-        MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false);
+        MessageArchiveService.Query query =
+                activity.xmppConnectionService
+                        .getMessageArchiveService()
+                        .query(conversation, new MamReference(0), timestamp, false);
         if (query != null) {
-            Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show();
+            Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG)
+                    .show();
         } else {
-            Toast.makeText(activity, R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show();
+            Toast.makeText(
+                            activity,
+                            R.string.not_fetching_history_retention_period,
+                            Toast.LENGTH_SHORT)
+                    .show();
         }
     }
 
     @Override
-    public View getView(int position, View view, ViewGroup parent) {
+    public View getView(final int position, View view, final @NonNull ViewGroup parent) {
         final Message message = getItem(position);
         final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
-        final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
+        final boolean isInValidSession =
+                message.isValidInSession() && (!omemoEncryption || message.isTrusted());
         final Conversational conversation = message.getConversation();
         final Account account = conversation.getAccount();
         final List<Element> commands = message.getCommands();
@@ -872,19 +985,22 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             viewHolder = new ViewHolder();
             switch (type) {
                 case DATE_SEPARATOR:
-                    view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false);
+                    view =
+                            activity.getLayoutInflater()
+                                    .inflate(R.layout.item_message_date_bubble, parent, false);
                     viewHolder.status_message = view.findViewById(R.id.message_body);
                     viewHolder.message_box = view.findViewById(R.id.message_box);
-                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
                     break;
                 case RTP_SESSION:
-                    view = activity.getLayoutInflater().inflate(R.layout.message_rtp_session, parent, false);
+                    view =
+                            activity.getLayoutInflater()
+                                    .inflate(R.layout.item_message_rtp_session, parent, false);
                     viewHolder.status_message = view.findViewById(R.id.message_body);
                     viewHolder.message_box = view.findViewById(R.id.message_box);
                     viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
                     break;
                 case SENT:
-                    view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false);
+                    view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false);
                     viewHolder.status_line = view.findViewById(R.id.status_line);
                     viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
                     viewHolder.message_box = view.findViewById(R.id.message_box);
@@ -901,7 +1017,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
                     break;
                 case RECEIVED:
-                    view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false);
+                    view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false);
                     viewHolder.status_line = view.findViewById(R.id.status_line);
                     viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
                     viewHolder.message_box = view.findViewById(R.id.message_box);
@@ -920,7 +1036,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
                     break;
                 case STATUS:
-                    view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
+                    view =
+                            activity.getLayoutInflater()
+                                    .inflate(R.layout.item_message_status, parent, false);
                     viewHolder.contact_picture = view.findViewById(R.id.message_photo);
                     viewHolder.status_message = view.findViewById(R.id.status_message);
                     viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
@@ -953,7 +1071,22 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             }
         }
 
-        boolean darkBackground = (type == RECEIVED && mUseGreenBackground) || activity.isDarkTheme();
+        final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black);
+        final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
+        final BubbleColor bubbleColor;
+        if (type == RECEIVED) {
+            if (isInValidSession) {
+                bubbleColor = colorfulBackground  || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
+            } else {
+                bubbleColor = BubbleColor.WARNING;
+            }
+        } else {
+            if (!colorfulBackground && black) {
+                bubbleColor = BubbleColor.SECONDARY;
+            } else {
+                bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
+            }
+        }
 
         if (type == DATE_SEPARATOR) {
             if (UIHelper.today(message.getTimeSent())) {
@@ -961,41 +1094,77 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             } else if (UIHelper.yesterday(message.getTimeSent())) {
                 viewHolder.status_message.setText(R.string.yesterday);
             } else {
-                viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
+                viewHolder.status_message.setText(
+                        DateUtils.formatDateTime(
+                                activity,
+                                message.getTimeSent(),
+                                DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
+            }
+            if (colorfulBackground) {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
+                setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
+            } else {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
+                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
             }
-            viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
             return view;
         } else if (type == RTP_SESSION) {
-            final boolean isDarkTheme = activity.isDarkTheme();
             final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
             final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
             final long duration = rtpSessionStatus.duration;
             final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent());
             if (received) {
                 if (duration > 0) {
-                    viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), callTime));
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.incoming_call_duration_timestamp,
+                                    TimeFrameUtils.resolve(activity, duration),
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
                 } else if (rtpSessionStatus.successful) {
                     viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime));
                 } else {
-                    viewHolder.status_message.setText(activity.getString(R.string.missed_call_timestamp, callTime));
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.missed_call_timestamp,
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
                 }
             } else {
                 if (duration > 0) {
-                    viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), callTime));
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.outgoing_call_duration_timestamp,
+                                    TimeFrameUtils.resolve(activity, duration),
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
                 } else {
-                    viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_timestamp, callTime));
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.outgoing_call_timestamp,
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
                 }
             }
-            viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful, isDarkTheme));
-            viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f);
-            viewHolder.message_box.setBackgroundResource(isDarkTheme ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
+            if (colorfulBackground) {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
+                setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
+                setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
+            } else {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
+                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
+                setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
+            }
+            viewHolder.indicatorReceived.setImageResource(
+                    RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
             return view;
         } else if (type == STATUS) {
             if ("LOAD_MORE".equals(message.getBody())) {
                 viewHolder.status_message.setVisibility(View.GONE);
                 viewHolder.contact_picture.setVisibility(View.GONE);
                 viewHolder.load_more_messages.setVisibility(View.VISIBLE);
-                viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation()));
+                viewHolder.load_more_messages.setOnClickListener(
+                        v -> loadMoreMessages((Conversation) message.getConversation()));
             } else {
                 viewHolder.status_message.setVisibility(View.VISIBLE);
                 viewHolder.load_more_messages.setVisibility(View.GONE);

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

@@ -3,7 +3,7 @@ package eu.siacs.conversations.ui.adapter;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.IntentSender;
-import android.graphics.PorterDuff;
+import android.content.res.ColorStateList;
 import android.view.ContextMenu;
 import android.view.LayoutInflater;
 import android.widget.TextView;
@@ -11,11 +11,16 @@ import android.view.View;
 import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
 import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.DiffUtil;
 import androidx.recyclerview.widget.ListAdapter;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.google.android.material.color.MaterialColors;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -23,7 +28,7 @@ import org.openintents.openpgp.util.OpenPgpUtils;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpEngine;
-import eu.siacs.conversations.databinding.ContactBinding;
+import eu.siacs.conversations.databinding.ItemContactBinding;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.services.XmppConnectionService;
@@ -68,7 +73,7 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
     @NonNull
     @Override
     public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
-        return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.contact, viewGroup, false));
+        return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_contact, viewGroup, false));
     }
 
     @Override
@@ -123,20 +128,32 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
             viewHolder.binding.key.setVisibility(View.GONE);
         }
 
+        final Context context = viewHolder.binding.getRoot().getContext();
+        final LayoutInflater inflater = LayoutInflater.from(context);
         viewHolder.binding.tags.setVisibility(View.VISIBLE);
-        viewHolder.binding.tags.removeAllViewsInLayout();
+        viewHolder.binding.tags.removeViews(1, viewHolder.binding.tags.getChildCount() - 1);
+        final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
         for (MucOptions.Hat hat : getPseudoHats(viewHolder.binding.getRoot().getContext(), user)) {
-            TextView tv = (TextView) LayoutInflater.from(viewHolder.binding.getRoot().getContext()).inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
-            tv.setText(hat.toString());
-            tv.getBackground().mutate().setColorFilter(hat.getColor(), PorterDuff.Mode.SRC_IN);
+            final String tag = hat.toString();
+            final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
+            tv.setText(tag);
+            tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(context,hat.getColor())));
+            final int id = ViewCompat.generateViewId();
+            tv.setId(id);
+            viewIdBuilder.add(id);
             viewHolder.binding.tags.addView(tv);
         }
         for (MucOptions.Hat hat : user.getHats()) {
-            TextView tv = (TextView) LayoutInflater.from(viewHolder.binding.getRoot().getContext()).inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
-            tv.setText(hat.toString());
-            tv.getBackground().mutate().setColorFilter(hat.getColor(), PorterDuff.Mode.SRC_IN);
+            final String tag = hat.toString();
+            final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
+            tv.setText(tag);
+            tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(context,hat.getColor())));
+            final int id = ViewCompat.generateViewId();
+            tv.setId(id);
+            viewIdBuilder.add(id);
             viewHolder.binding.tags.addView(tv);
         }
+        viewHolder.binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
 
         if (viewHolder.binding.tags.getChildCount() < 1) {
             viewHolder.binding.contactJid.setVisibility(View.VISIBLE);
@@ -164,11 +181,11 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
         MucDetailsContextMenuHelper.onCreateContextMenu(menu,v);
     }
 
-    class ViewHolder extends RecyclerView.ViewHolder {
+    static class ViewHolder extends RecyclerView.ViewHolder {
 
-        private final ContactBinding binding;
+        private final ItemContactBinding binding;
 
-        private ViewHolder(ContactBinding binding) {
+        private ViewHolder(ItemContactBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
         }

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

@@ -11,13 +11,14 @@ import androidx.recyclerview.widget.ListAdapter;
 import androidx.recyclerview.widget.RecyclerView;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.databinding.UserPreviewBinding;
+import eu.siacs.conversations.databinding.ItemUserPreviewBinding;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
 
-public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreviewAdapter.ViewHolder> implements View.OnCreateContextMenuListener {
+public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreviewAdapter.ViewHolder>
+        implements View.OnCreateContextMenuListener {
 
     private MucOptions.User selectedUser = null;
 
@@ -28,29 +29,43 @@ public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreview
     @NonNull
     @Override
     public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
-        return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.user_preview, viewGroup, false));
+        return new ViewHolder(
+                DataBindingUtil.inflate(
+                        LayoutInflater.from(viewGroup.getContext()),
+                        R.layout.item_user_preview,
+                        viewGroup,
+                        false));
     }
 
     @Override
     public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
         final MucOptions.User user = getItem(position);
         AvatarWorkerTask.loadAvatar(user, viewHolder.binding.avatar, R.dimen.media_size);
-        viewHolder.binding.getRoot().setOnClickListener(v -> {
-            final XmppActivity activity = XmppActivity.find(v);
-            if (activity != null) {
-                activity.highlightInMuc(user.getConversation(), user.getNick());
-            }
-        });
+        viewHolder
+                .binding
+                .getRoot()
+                .setOnClickListener(
+                        v -> {
+                            final XmppActivity activity = XmppActivity.find(v);
+                            if (activity != null) {
+                                activity.highlightInMuc(user.getConversation(), user.getNick());
+                            }
+                        });
         viewHolder.binding.getRoot().setOnCreateContextMenuListener(this);
         viewHolder.binding.getRoot().setTag(user);
-        viewHolder.binding.getRoot().setOnLongClickListener(v -> {
-            selectedUser = user;
-            return false;
-        });
+        viewHolder
+                .binding
+                .getRoot()
+                .setOnLongClickListener(
+                        v -> {
+                            selectedUser = user;
+                            return false;
+                        });
     }
 
     @Override
-    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+    public void onCreateContextMenu(
+            ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
         MucDetailsContextMenuHelper.onCreateContextMenu(menu, v);
     }
 
@@ -60,9 +75,9 @@ public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreview
 
     class ViewHolder extends RecyclerView.ViewHolder {
 
-        private final UserPreviewBinding binding;
+        private final ItemUserPreviewBinding binding;
 
-        private ViewHolder(UserPreviewBinding binding) {
+        private ViewHolder(final ItemUserPreviewBinding binding) {
             super(binding.getRoot());
             this.binding = binding;
         }

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

@@ -1,80 +0,0 @@
-package eu.siacs.conversations.ui.forms;
-
-import android.content.Context;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.xmpp.forms.Field;
-
-public class FormBooleanFieldWrapper extends FormFieldWrapper {
-
-	protected CheckBox checkBox;
-
-	protected FormBooleanFieldWrapper(Context context, Field field) {
-		super(context, field);
-		checkBox = view.findViewById(R.id.field);
-		checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-			@Override
-			public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-				checkBox.setError(null);
-				invokeOnFormFieldValuesEdited();
-			}
-		});
-	}
-
-	@Override
-	protected void setLabel(String label, boolean required) {
-		CheckBox checkBox = view.findViewById(R.id.field);
-		checkBox.setText(createSpannableLabelString(label, required));
-	}
-
-	@Override
-	public List<String> getValues() {
-		List<String> values = new ArrayList<>();
-		values.add(Boolean.toString(checkBox.isChecked()));
-		return values;
-	}
-
-	@Override
-	protected void setValues(List<String> values) {
-		if (values.size() == 0) {
-			checkBox.setChecked(false);
-		} else {
-			checkBox.setChecked(Boolean.parseBoolean(values.get(0)));
-		}
-	}
-
-	@Override
-	public boolean validates() {
-		if (checkBox.isChecked() || !field.isRequired()) {
-			return true;
-		} else {
-			checkBox.setError(context.getString(R.string.this_field_is_required));
-			checkBox.requestFocus();
-			return false;
-		}
-	}
-
-	@Override
-	public boolean edited() {
-		if (field.getValues().size() == 0) {
-			return checkBox.isChecked();
-		} else {
-			return super.edited();
-		}
-	}
-
-	@Override
-	protected int getLayoutResource() {
-		return R.layout.form_boolean;
-	}
-
-	@Override
-	void setReadOnly(boolean readOnly) {
-		checkBox.setEnabled(!readOnly);
-	}
-}

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

@@ -1,30 +0,0 @@
-package eu.siacs.conversations.ui.forms;
-
-import android.content.Context;
-
-import java.util.Hashtable;
-
-import eu.siacs.conversations.xmpp.forms.Field;
-
-
-
-public class FormFieldFactory {
-
-	private static final Hashtable<String, Class> typeTable = new Hashtable<>();
-
-	static {
-		typeTable.put("text-single", FormTextFieldWrapper.class);
-		typeTable.put("text-multi", FormTextFieldWrapper.class);
-		typeTable.put("text-private", FormTextFieldWrapper.class);
-		typeTable.put("jid-single", FormJidSingleFieldWrapper.class);
-		typeTable.put("boolean", FormBooleanFieldWrapper.class);
-	}
-
-	protected static FormFieldWrapper createFromField(Context context, Field field) {
-		Class clazz = typeTable.get(field.getType());
-		if (clazz == null) {
-			clazz = FormTextFieldWrapper.class;
-		}
-		return FormFieldWrapper.createFromField(clazz, context, field);
-	}
-}

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

@@ -1,93 +0,0 @@
-package eu.siacs.conversations.ui.forms;
-
-import android.content.Context;
-import android.text.SpannableString;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import java.util.List;
-
-import eu.siacs.conversations.ui.util.StyledAttributes;
-import eu.siacs.conversations.xmpp.forms.Field;
-
-public abstract class FormFieldWrapper {
-
-	protected final Context context;
-	protected final Field field;
-	protected final View view;
-	OnFormFieldValuesEdited onFormFieldValuesEditedListener;
-
-	FormFieldWrapper(Context context, Field field) {
-		this.context = context;
-		this.field = field;
-		LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-		this.view = inflater.inflate(getLayoutResource(), null);
-		String label = field.getLabel();
-		if (label == null) {
-			label = field.getFieldName();
-		}
-		setLabel(label, field.isRequired());
-	}
-
-	public final void submit() {
-		this.field.setValues(getValues());
-	}
-
-	public final View getView() {
-		return view;
-	}
-
-	protected abstract void setLabel(String label, boolean required);
-
-	abstract List<String> getValues();
-
-	protected abstract void setValues(List<String> values);
-
-	abstract boolean validates();
-
-	abstract protected int getLayoutResource();
-
-	abstract void setReadOnly(boolean readOnly);
-
-	protected SpannableString createSpannableLabelString(String label, boolean required) {
-		SpannableString spannableString = new SpannableString(label + (required ? " *" : ""));
-		if (required) {
-			int start = label.length();
-			int end = label.length() + 2;
-			spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0);
-			spannableString.setSpan(new ForegroundColorSpan(StyledAttributes.getColor(context, androidx.appcompat.R.attr.colorAccent)), start, end, 0);
-		}
-		return spannableString;
-	}
-
-	protected void invokeOnFormFieldValuesEdited() {
-		if (this.onFormFieldValuesEditedListener != null) {
-			this.onFormFieldValuesEditedListener.onFormFieldValuesEdited();
-		}
-	}
-
-	public boolean edited() {
-		return !field.getValues().equals(getValues());
-	}
-
-	public void setOnFormFieldValuesEditedListener(OnFormFieldValuesEdited listener) {
-		this.onFormFieldValuesEditedListener = listener;
-	}
-
-	protected static <F extends FormFieldWrapper> FormFieldWrapper createFromField(Class<F> c, Context context, Field field) {
-		try {
-			F fieldWrapper = c.getDeclaredConstructor(Context.class, Field.class).newInstance(context,field);
-			fieldWrapper.setValues(field.getValues());
-			return fieldWrapper;
-		} catch (Exception e) {
-			e.printStackTrace();
-			return null;
-		}
-	}
-
-	public interface OnFormFieldValuesEdited {
-		void onFormFieldValuesEdited();
-	}
-}

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

@@ -1,43 +0,0 @@
-package eu.siacs.conversations.ui.forms;
-
-import android.content.Context;
-import android.text.InputType;
-
-import java.util.List;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.forms.Field;
-
-public class FormJidSingleFieldWrapper extends FormTextFieldWrapper {
-
-	protected FormJidSingleFieldWrapper(Context context, Field field) {
-		super(context, field);
-		editText.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
-		editText.setHint(R.string.account_settings_example_jabber_id);
-	}
-
-	@Override
-	public boolean validates() {
-		String value = getValue();
-		if (!value.isEmpty()) {
-			try {
-				Jid.of(value);
-			} catch (IllegalArgumentException e) {
-				editText.setError(context.getString(R.string.invalid_jid));
-				editText.requestFocus();
-				return false;
-			}
-		}
-		return super.validates();
-	}
-
-	@Override
-	protected void setValues(List<String> values) {
-		StringBuilder builder = new StringBuilder();
-		for(String value : values) {
-			builder.append(value);
-		}
-		editText.setText(builder.toString());
-	}
-}

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

@@ -1,97 +0,0 @@
-package eu.siacs.conversations.ui.forms;
-
-import android.content.Context;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextWatcher;
-import android.widget.EditText;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.xmpp.forms.Field;
-
-public class FormTextFieldWrapper extends FormFieldWrapper {
-
-	protected EditText editText;
-
-	protected FormTextFieldWrapper(Context context, Field field) {
-		super(context, field);
-		editText = view.findViewById(R.id.field);
-		editText.setSingleLine(!"text-multi".equals(field.getType()));
-		if ("text-private".equals(field.getType())) {
-			editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
-		}
-		editText.addTextChangedListener(new TextWatcher() {
-			@Override
-			public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-			}
-
-			@Override
-			public void onTextChanged(CharSequence s, int start, int before, int count) {
-				editText.setError(null);
-				invokeOnFormFieldValuesEdited();
-			}
-
-			@Override
-			public void afterTextChanged(Editable s) {
-			}
-		});
-	}
-
-	@Override
-	protected void setLabel(String label, boolean required) {
-		TextView textView = view.findViewById(R.id.label);
-		textView.setText(createSpannableLabelString(label, required));
-	}
-
-	protected String getValue() {
-		return editText.getText().toString();
-	}
-
-	@Override
-	public List<String> getValues() {
-		List<String> values = new ArrayList<>();
-		for (String line : getValue().split("\\n")) {
-			if (line.length() > 0) {
-				values.add(line);
-			}
-		}
-		return values;
-	}
-
-	@Override
-	protected void setValues(List<String> values) {
-		StringBuilder builder = new StringBuilder();
-		for(int i = 0; i < values.size(); ++i) {
-			builder.append(values.get(i));
-			if (i < values.size() - 1 && "text-multi".equals(field.getType())) {
-				builder.append("\n");
-			}
-		}
-		editText.setText(builder.toString());
-	}
-
-	@Override
-	public boolean validates() {
-		if (getValue().trim().length() > 0 || !field.isRequired()) {
-			return true;
-		} else {
-			editText.setError(context.getString(R.string.this_field_is_required));
-			editText.requestFocus();
-			return false;
-		}
-	}
-
-	@Override
-	protected int getLayoutResource() {
-		return R.layout.form_text;
-	}
-
-	@Override
-	void setReadOnly(boolean readOnly) {
-		editText.setEnabled(!readOnly);
-	}
-}

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

@@ -1,72 +0,0 @@
-package eu.siacs.conversations.ui.forms;
-
-import android.content.Context;
-import android.widget.LinearLayout;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.forms.Field;
-
-public class FormWrapper {
-
-	private final LinearLayout layout;
-
-	private final Data form;
-
-	private final List<FormFieldWrapper> fieldWrappers = new ArrayList<>();
-
-	private FormWrapper(Context context, LinearLayout linearLayout, Data form) {
-		this.form = form;
-		this.layout = linearLayout;
-		this.layout.removeAllViews();
-		for(Field field : form.getFields()) {
-			FormFieldWrapper fieldWrapper = FormFieldFactory.createFromField(context,field);
-			if (fieldWrapper != null) {
-				layout.addView(fieldWrapper.getView());
-				fieldWrappers.add(fieldWrapper);
-			}
-		}
-	}
-
-	public Data submit() {
-		for(FormFieldWrapper fieldWrapper : fieldWrappers) {
-			fieldWrapper.submit();
-		}
-		this.form.submit();
-		return this.form;
-	}
-
-	public boolean validates() {
-		boolean validates = true;
-		for(FormFieldWrapper fieldWrapper : fieldWrappers) {
-			validates &= fieldWrapper.validates();
-		}
-		return validates;
-	}
-
-	public void setOnFormFieldValuesEditedListener(FormFieldWrapper.OnFormFieldValuesEdited listener) {
-		for(FormFieldWrapper fieldWrapper : fieldWrappers) {
-			fieldWrapper.setOnFormFieldValuesEditedListener(listener);
-		}
-	}
-
-	public void setReadOnly(boolean b) {
-		for(FormFieldWrapper fieldWrapper : fieldWrappers) {
-			fieldWrapper.setReadOnly(b);
-		}
-	}
-
-	public boolean edited() {
-		boolean edited = false;
-		for(FormFieldWrapper fieldWrapper : fieldWrappers) {
-			edited |= fieldWrapper.edited();
-		}
-		return edited;
-	}
-
-	public static FormWrapper createInLayout(Context context, LinearLayout layout, Data form) {
-		return new FormWrapper(context, layout, form);
-	}
-}

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

@@ -0,0 +1,98 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Build;
+import android.os.storage.StorageManager;
+import android.preference.PreferenceManager;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.cheogram.android.DownloadDefaultStickers;
+
+import eu.siacs.conversations.R;
+
+public class AttachmentsSettingsFragment extends XmppPreferenceFragment {
+
+    private final ActivityResultLauncher<String> requestStorageLauncher =
+            registerForActivityResult(
+                    new ActivityResultContracts.RequestPermission(),
+                    isGranted -> {
+                        if (isGranted) {
+                            downloadStickers();
+                        } else {
+                            Toast.makeText(
+                                            requireActivity(),
+                                            getString(
+                                                    R.string.no_storage_permission,
+                                                    getString(R.string.app_name)),
+                                            Toast.LENGTH_LONG)
+                                    .show();
+                        }
+                    });
+
+    @Override
+    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_attachments, rootKey);
+
+        final var p = PreferenceManager.getDefaultSharedPreferences(requireActivity());
+        final var stickerDir = findPreference("sticker_directory");
+        stickerDir.setSummary(p.getString("sticker_directory", "Pictures/Stickers"));
+        stickerDir.setOnPreferenceClickListener((pref) -> {
+            final var intent = ((StorageManager) requireActivity().getSystemService(Context.STORAGE_SERVICE)).getPrimaryStorageVolume().createOpenDocumentTreeIntent();
+            startActivityForResult(Intent.createChooser(intent, "Choose sticker location"), 0);
+            return true;
+        });
+
+        final var downloadDefaultStickers = findPreference("download_default_stickers");
+        downloadDefaultStickers.setOnPreferenceClickListener((pref) -> {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+                if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+                    requestStorageLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+                } else {
+                    downloadStickers();
+                }
+            } else {
+                downloadStickers();
+            }
+            return true;
+        });
+
+        final var clearBlockedMedia = findPreference("clear_blocked_media");
+        clearBlockedMedia.setOnPreferenceClickListener((pref) -> {
+            requireService().clearBlockedMedia();
+            runOnUiThread(() -> Toast.makeText(requireActivity(), "Blocked media will be displayed again", Toast.LENGTH_LONG).show());
+            return true;
+        });
+    }
+
+    protected void downloadStickers() {
+        final var intent = new Intent(requireActivity(), DownloadDefaultStickers.class);
+        intent.putExtra("tor", requireService().useTorToConnect());
+        ContextCompat.startForegroundService(requireActivity(), intent);
+        runOnUiThread(() -> Toast.makeText(requireActivity(), "Sticker download started", Toast.LENGTH_LONG).show());
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.pref_attachments);
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (data == null || data.getData() == null) return;
+
+        final var p = PreferenceManager.getDefaultSharedPreferences(requireActivity());
+        p.edit().putString("sticker_directory", data.getData().toString()).commit();
+        final var stickerDir = findPreference("sticker_directory");
+        stickerDir.setSummary(data.getData().toString());
+    }
+}

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

@@ -0,0 +1,73 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.services.QuickConversationsService;
+
+public class ConnectionSettingsFragment extends XmppPreferenceFragment {
+
+    private static final String GROUPS_AND_CONFERENCES = "groups_and_conferences";
+
+    public static boolean hideChannelDiscovery() {
+        return QuickConversationsService.isQuicksy()
+                || QuickConversationsService.isPlayStoreFlavor()
+                || Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY);
+    }
+
+    @Override
+    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_connection, rootKey);
+        final var connectionOptions = findPreference(AppSettings.SHOW_CONNECTION_OPTIONS);
+        final var channelDiscovery = findPreference(AppSettings.CHANNEL_DISCOVERY_METHOD);
+        final var groupsAndConferences = findPreference(GROUPS_AND_CONFERENCES);
+        if (connectionOptions == null || channelDiscovery == null || groupsAndConferences == null) {
+            throw new IllegalStateException();
+        }
+        if (QuickConversationsService.isQuicksy()) {
+            connectionOptions.setVisible(false);
+        }
+        if (hideChannelDiscovery()) {
+            groupsAndConferences.setVisible(false);
+            channelDiscovery.setVisible(false);
+        }
+    }
+
+    @Override
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        super.onSharedPreferenceChanged(key);
+        switch (key) {
+            case AppSettings.USE_TOR -> {
+                final var appSettings = new AppSettings(requireContext());
+                if (appSettings.isUseTor()) {
+                    runOnUiThread(
+                            () ->
+                                    Toast.makeText(
+                                                    requireActivity(),
+                                                    R.string.audio_video_disabled_tor,
+                                                    Toast.LENGTH_LONG)
+                                            .show());
+                }
+                reconnectAccounts();
+                requireService().reinitializeMuclumbusService();
+            }
+            case AppSettings.SHOW_CONNECTION_OPTIONS -> {
+                reconnectAccounts();
+            }
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.pref_connection_options);
+    }
+}

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

@@ -0,0 +1,106 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.os.Bundle;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.material.color.DynamicColors;
+
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Conversations;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.activity.SettingsActivity;
+import eu.siacs.conversations.ui.util.SettingsUtils;
+import eu.siacs.conversations.utils.ThemeHelper;
+
+public class InterfaceSettingsFragment extends XmppPreferenceFragment {
+
+    @Override
+    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_interface, rootKey);
+        final var themePreference = findPreference("theme");
+        final var dynamicColors = findPreference("dynamic_colors");
+        if (themePreference == null || dynamicColors == null) {
+            throw new IllegalStateException(
+                    "The preference resource file did not contain theme or color preferences");
+        }
+        themePreference.setOnPreferenceChangeListener(
+                (preference, newValue) -> {
+                    if (newValue instanceof final String theme) {
+                        requireSettingsActivity().recreate();
+                    }
+                    updateCustomVisibility("custom".equals(newValue));
+                    return true;
+                });
+        updateCustomVisibility("Custom".equals(themePreference.getSummary()));
+        dynamicColors.setOnPreferenceChangeListener(
+                (preference, newValue) -> {
+                    requireSettingsActivity().setDynamicColors(Boolean.TRUE.equals(newValue));
+                    return true;
+                });
+    }
+
+    protected void updateCustomVisibility(boolean custom) {
+        custom = custom && (Build.VERSION.SDK_INT >= 30);
+
+        final var dark = requireSettingsActivity().isDark();
+        final var sharedPreferences = getPreferenceManager().getSharedPreferences();
+        findPreference("custom_theme_automatic").setVisible(custom);
+        findPreference("custom_theme_dark").setVisible(custom && !sharedPreferences.getBoolean("custom_theme_automatic", true));
+
+        findPreference("custom_theme_primary").setVisible(custom && !dark);
+        findPreference("custom_theme_primary_dark").setVisible(custom && !dark);
+        findPreference("custom_theme_accent").setVisible(custom && !dark);
+        findPreference("custom_theme_background_primary").setVisible(custom && !dark);
+
+        findPreference("custom_dark_theme_primary").setVisible(custom && dark);
+        findPreference("custom_dark_theme_primary_dark").setVisible(custom && dark);
+        findPreference("custom_dark_theme_accent").setVisible(custom && dark);
+        findPreference("custom_dark_theme_background_primary").setVisible(custom && dark);
+
+        findPreference("dynamic_colors").setVisible(DynamicColors.isDynamicColorAvailable() && !custom);
+    }
+
+    @Override
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        super.onSharedPreferenceChanged(key);
+        if (key.equals(AppSettings.ALLOW_SCREENSHOTS)) {
+            SettingsUtils.applyScreenshotSetting(requireActivity());
+        }
+
+        if (
+            key.equals("custom_theme_automatic") ||
+            key.equals("custom_theme_dark") ||
+            key.equals("custom_theme_primary") ||
+            key.equals("custom_theme_primary_dark") ||
+            key.equals("custom_theme_accent") ||
+            key.equals("custom_theme_background_primary") ||
+            key.equals("custom_dark_theme_primary") ||
+            key.equals("custom_dark_theme_primary_dark") ||
+            key.equals("custom_dark_theme_accent") ||
+            key.equals("custom_dark_theme_background_primary"))
+        {
+            ThemeHelper.applyCustomColors(requireService());
+            new Thread(() -> runOnUiThread(() -> requireActivity().recreate())).start();
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.pref_title_interface);
+    }
+
+    public SettingsActivity requireSettingsActivity() {
+        final var activity = requireActivity();
+        if (activity instanceof SettingsActivity settingsActivity) {
+            return settingsActivity;
+        }
+        throw new IllegalStateException(
+                String.format(
+                        "%s is not %s",
+                        activity.getClass().getName(), SettingsActivity.class.getName()));
+    }
+}

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

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

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

@@ -0,0 +1,161 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.app.NotificationChannel;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.google.common.base.Optional;
+
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.services.NotificationService;
+import eu.siacs.conversations.ui.activity.result.PickRingtone;
+import eu.siacs.conversations.utils.Compatibility;
+
+public class NotificationsSettingsFragment extends XmppPreferenceFragment {
+
+    private final ActivityResultLauncher<Uri> pickNotificationToneLauncher =
+            registerForActivityResult(
+                    new PickRingtone(RingtoneManager.TYPE_NOTIFICATION),
+                    result -> {
+                        if (result == null) {
+                            // do nothing. user aborted
+                            return;
+                        }
+                        final Uri uri = PickRingtone.noneToNull(result);
+                        appSettings().setNotificationTone(uri);
+                        Log.i(Config.LOGTAG, "User set notification tone to " + uri);
+                    });
+    private final ActivityResultLauncher<Uri> pickRingtoneLauncher =
+            registerForActivityResult(
+                    new PickRingtone(RingtoneManager.TYPE_RINGTONE),
+                    result -> {
+                        if (result == null) {
+                            // do nothing. user aborted
+                            return;
+                        }
+                        final Uri uri = PickRingtone.noneToNull(result);
+                        appSettings().setRingtone(uri);
+                        Log.i(Config.LOGTAG, "User set ringtone to " + uri);
+                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                            NotificationService.recreateIncomingCallChannel(requireContext(), uri);
+                        }
+                    });
+
+    @Override
+    public void onCreatePreferences(
+            @Nullable final Bundle savedInstanceState, final @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_notifications, rootKey);
+        final var messageNotificationSettings = findPreference("message_notification_settings");
+        final var notificationRingtone = findPreference(AppSettings.NOTIFICATION_RINGTONE);
+        final var notificationHeadsUp = findPreference(AppSettings.NOTIFICATION_HEADS_UP);
+        final var notificationVibrate = findPreference(AppSettings.NOTIFICATION_VIBRATE);
+        final var notificationLed = findPreference(AppSettings.NOTIFICATION_LED);
+        final var foregroundService = findPreference(AppSettings.KEEP_FOREGROUND_SERVICE);
+        if (messageNotificationSettings == null
+                || notificationRingtone == null
+                || notificationHeadsUp == null
+                || notificationVibrate == null
+                || notificationLed == null
+                || foregroundService == null) {
+            throw new IllegalStateException("The preference resource file is missing preferences");
+        }
+        if (Compatibility.runsTwentySix()) {
+            notificationRingtone.setVisible(false);
+            notificationHeadsUp.setVisible(false);
+            notificationVibrate.setVisible(false);
+            notificationLed.setVisible(false);
+            foregroundService.setVisible(false);
+        } else {
+            messageNotificationSettings.setVisible(false);
+        }
+    }
+
+    @Override
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        super.onSharedPreferenceChanged(key);
+        if (key.equals(AppSettings.KEEP_FOREGROUND_SERVICE)) {
+            requireService().toggleForegroundService();
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.notifications);
+    }
+
+    @Override
+    public void onBackendConnected() {
+        boolean diallerIntegrationPossible = false;
+
+        if (Build.VERSION.SDK_INT >= 23) {
+            outer:
+            for (final var account : requireService().getAccounts()) {
+                for (final var contact : account.getRoster().getContacts()) {
+                    if (contact.getPresences().anyIdentity("gateway", "pstn")) {
+                        diallerIntegrationPossible = true;
+                        break outer;
+                    }
+                }
+            }
+        }
+        if (!diallerIntegrationPossible) {
+            final var pref = findPreference("dialler_integration_incoming");
+            pref.setVisible(false);
+        }
+    }
+
+    @Override
+    public boolean onPreferenceTreeClick(final Preference preference) {
+        final var key = preference.getKey();
+        if (AppSettings.RINGTONE.equals(key)) {
+            pickRingtone();
+            return true;
+        }
+        if (AppSettings.NOTIFICATION_RINGTONE.equals(key)) {
+            pickNotificationTone();
+            return true;
+        }
+        return super.onPreferenceTreeClick(preference);
+    }
+
+    private void pickNotificationTone() {
+        final Uri uri = appSettings().getNotificationTone();
+        Log.i(Config.LOGTAG, "current notification tone: " + uri);
+        this.pickNotificationToneLauncher.launch(uri);
+    }
+
+    private void pickRingtone() {
+        final Optional<Uri> channelRingtone;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            channelRingtone =
+                    NotificationService.getCurrentIncomingCallChannel(requireContext())
+                            .transform(NotificationChannel::getSound);
+        } else {
+            channelRingtone = Optional.absent();
+        }
+        final Uri uri;
+        if (channelRingtone.isPresent()) {
+            uri = channelRingtone.get();
+            Log.d(Config.LOGTAG, "ringtone came from channel");
+        } else {
+            uri = appSettings().getRingtone();
+        }
+        Log.i(Config.LOGTAG, "current ringtone: " + uri);
+        this.pickRingtoneLauncher.launch(uri);
+    }
+
+    private AppSettings appSettings() {
+        return new AppSettings(requireContext());
+    }
+}

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

@@ -0,0 +1,46 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.R;
+
+public class PrivacySettingsFragment extends XmppPreferenceFragment {
+
+    @Override
+    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_privacy, rootKey);
+        try {
+            Class.forName("io.sentry.Sentry");
+            final var neverSend = findPreference("never_send");
+            neverSend.setVisible(false);
+        } catch (final ClassNotFoundException e) { }
+    }
+
+    @Override
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        super.onSharedPreferenceChanged(key);
+        switch (key) {
+            case AppSettings.AWAY_WHEN_SCREEN_IS_OFF, AppSettings.MANUALLY_CHANGE_PRESENCE -> {
+                requireService().toggleScreenEventReceiver();
+                requireService().refreshAllPresences();
+            }
+            case AppSettings.CONFIRM_MESSAGES,
+                    AppSettings.BROADCAST_LAST_ACTIVITY,
+                    AppSettings.ALLOW_MESSAGE_CORRECTION,
+                    AppSettings.DND_ON_SILENT_MODE,
+                    AppSettings.TREAT_VIBRATE_AS_SILENT -> {
+                requireService().refreshAllPresences();
+            }
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.pref_privacy);
+    }
+}

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

@@ -0,0 +1,200 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
+
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.OmemoSetting;
+import eu.siacs.conversations.services.MemorizingTrustManager;
+import eu.siacs.conversations.utils.TimeFrameUtils;
+
+import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class SecuritySettingsFragment extends XmppPreferenceFragment {
+
+    private static final String REMOVE_TRUSTED_CERTIFICATES = "remove_trusted_certificates";
+
+    @Override
+    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_security, rootKey);
+        final ListPreference omemo = findPreference(AppSettings.OMEMO);
+        final ListPreference automaticMessageDeletion =
+                findPreference(AppSettings.AUTOMATIC_MESSAGE_DELETION);
+        if (omemo == null || automaticMessageDeletion == null) {
+            throw new IllegalStateException("The preference resource file is missing preferences");
+        }
+        omemo.setSummaryProvider(new OmemoSummaryProvider());
+        final int[] choices = getResources().getIntArray(R.array.automatic_message_deletion_values);
+        final CharSequence[] entries = new CharSequence[choices.length];
+        final CharSequence[] entryValues = new CharSequence[choices.length];
+        for (int i = 0; i < choices.length; ++i) {
+            entryValues[i] = String.valueOf(choices[i]);
+            entries[i] = messageDeletionValueToName(requireContext(), choices[i]);
+        }
+        automaticMessageDeletion.setEntries(entries);
+        automaticMessageDeletion.setEntryValues(entryValues);
+        automaticMessageDeletion.setSummaryProvider(new MessageDeletionSummaryProvider());
+    }
+
+    private static String messageDeletionValueToName(final Context context, final int value) {
+        if (value == 0) {
+            return context.getString(R.string.never);
+        } else {
+            return TimeFrameUtils.resolve(context, 1000L * value);
+        }
+    }
+
+    @Override
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        super.onSharedPreferenceChanged(key);
+        switch (key) {
+            case AppSettings.OMEMO -> {
+                OmemoSetting.load(requireContext());
+            }
+            case AppSettings.TRUST_SYSTEM_CA_STORE -> {
+                requireService().updateMemorizingTrustManager();
+                reconnectAccounts();
+            }
+            case AppSettings.REQUIRE_CHANNEL_BINDING -> {
+                reconnectAccounts();
+            }
+            case AppSettings.AUTOMATIC_MESSAGE_DELETION -> {
+                requireService().expireOldMessages(true);
+            }
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.pref_title_security);
+    }
+
+    @Override
+    public boolean onPreferenceTreeClick(final Preference preference) {
+        if (REMOVE_TRUSTED_CERTIFICATES.equals(preference.getKey())) {
+            showRemoveCertificatesDialog();
+            return true;
+        }
+        return super.onPreferenceTreeClick(preference);
+    }
+
+    private void showRemoveCertificatesDialog() {
+        final MemorizingTrustManager mtm = requireService().getMemorizingTrustManager();
+        final ArrayList<String> aliases = Collections.list(mtm.getCertificates());
+        if (aliases.isEmpty()) {
+            Toast.makeText(requireActivity(), R.string.toast_no_trusted_certs, Toast.LENGTH_LONG)
+                    .show();
+            return;
+        }
+        final ArrayList<Integer> selectedItems = new ArrayList<>();
+        final MaterialAlertDialogBuilder dialogBuilder =
+                new MaterialAlertDialogBuilder(requireActivity());
+        dialogBuilder.setTitle(getString(R.string.dialog_manage_certs_title));
+        dialogBuilder.setMultiChoiceItems(
+                aliases.toArray(new CharSequence[0]),
+                null,
+                (dialog, indexSelected, isChecked) -> {
+                    if (isChecked) {
+                        selectedItems.add(indexSelected);
+                    } else if (selectedItems.contains(indexSelected)) {
+                        selectedItems.remove(Integer.valueOf(indexSelected));
+                    }
+                    if (dialog instanceof AlertDialog alertDialog) {
+                        alertDialog
+                                .getButton(DialogInterface.BUTTON_POSITIVE)
+                                .setEnabled(!selectedItems.isEmpty());
+                    }
+                });
+
+        dialogBuilder.setPositiveButton(
+                getString(R.string.dialog_manage_certs_positivebutton),
+                (dialog, which) -> confirmCertificateDeletion(aliases, selectedItems));
+        dialogBuilder.setNegativeButton(R.string.cancel, null);
+        final AlertDialog removeCertsDialog = dialogBuilder.create();
+        removeCertsDialog.show();
+        removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+    }
+
+    private void confirmCertificateDeletion(
+            final ArrayList<String> aliases, final ArrayList<Integer> selectedItems) {
+        final int count = selectedItems.size();
+        if (count == 0) {
+            return;
+        }
+        final MemorizingTrustManager mtm = requireService().getMemorizingTrustManager();
+        for (int i = 0; i < count; i++) {
+            try {
+                final int item = Integer.parseInt(selectedItems.get(i).toString());
+                final String alias = aliases.get(item);
+                mtm.deleteCertificate(alias);
+            } catch (final KeyStoreException e) {
+                Toast.makeText(
+                                requireActivity(),
+                                "Error: " + e.getLocalizedMessage(),
+                                Toast.LENGTH_LONG)
+                        .show();
+            }
+        }
+        reconnectAccounts();
+        Toast.makeText(
+                        requireActivity(),
+                        getResources()
+                                .getQuantityString(
+                                        R.plurals.toast_delete_certificates, count, count),
+                        Toast.LENGTH_LONG)
+                .show();
+    }
+
+    private static class MessageDeletionSummaryProvider
+            implements Preference.SummaryProvider<ListPreference> {
+
+        @Nullable
+        @Override
+        public CharSequence provideSummary(@NonNull ListPreference preference) {
+            final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue()));
+            return messageDeletionValueToName(preference.getContext(), value == null ? 0 : value);
+        }
+    }
+
+    private static class OmemoSummaryProvider
+            implements Preference.SummaryProvider<ListPreference> {
+
+        @Nullable
+        @Override
+        public CharSequence provideSummary(@NonNull ListPreference preference) {
+            final var context = preference.getContext();
+            final var sharedPreferences = preference.getSharedPreferences();
+            final String value;
+            if (sharedPreferences == null) {
+                value = null;
+            } else {
+                value =
+                        sharedPreferences.getString(
+                                preference.getKey(),
+                                context.getString(R.string.omemo_setting_default));
+            }
+            return switch (Strings.nullToEmpty(value)) {
+                case "always" -> context.getString(R.string.pref_omemo_setting_summary_always);
+                case "default_off" -> context.getString(
+                        R.string.pref_omemo_setting_summary_default_off);
+                default -> context.getString(R.string.pref_omemo_setting_summary_default_on);
+            };
+        }
+    }
+}

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

@@ -0,0 +1,109 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.EditTextPreference;
+import androidx.preference.ListPreference;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.services.UnifiedPushDistributor;
+import eu.siacs.conversations.xmpp.Jid;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+
+public class UpSettingsFragment extends XmppPreferenceFragment {
+
+    @Override
+    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_up, rootKey);
+    }
+
+    @Override
+    public void onBackendConnected() {
+        final ListPreference upAccounts = findPreference(UnifiedPushDistributor.PREFERENCE_ACCOUNT);
+        final EditTextPreference pushServer = findPreference(UnifiedPushDistributor.PREFERENCE_PUSH_SERVER);
+        if (upAccounts == null || pushServer == null) {
+            throw new IllegalStateException();
+        }
+        pushServer.setOnPreferenceChangeListener((preference, newValue) -> {
+            if (newValue instanceof String string) {
+                if (Strings.isNullOrEmpty(string) || isJidInvalid(string) || isHttpUri(string)) {
+                    Toast.makeText(requireActivity(),R.string.invalid_jid,Toast.LENGTH_LONG).show();
+                    return false;
+                } else {
+                    return true;
+                }
+            } else {
+                Toast.makeText(requireActivity(),R.string.invalid_jid,Toast.LENGTH_LONG).show();
+                return false;
+            }
+        });
+        reconfigureUpAccountPreference(upAccounts);
+    }
+
+    private static boolean isJidInvalid(final String input) {
+        try {
+            final var jid = Jid.ofEscaped(input);
+            return !jid.isBareJid();
+        } catch (final IllegalArgumentException e) {
+            return true;
+        }
+    }
+
+    private static boolean isHttpUri(final String input) {
+        final URI uri;
+        try {
+            uri = new URI(input);
+        } catch (final URISyntaxException e) {
+            return false;
+        }
+        return Arrays.asList("http","https").contains(uri.getScheme());
+    }
+
+
+    private void reconfigureUpAccountPreference(final ListPreference listPreference) {
+        final List<CharSequence> accounts =
+                ImmutableList.copyOf(
+                        Lists.transform(
+                                requireService().getAccounts(),
+                                a -> a.getJid().asBareJid().toEscapedString()));
+        final ImmutableList.Builder<CharSequence> entries = new ImmutableList.Builder<>();
+        final ImmutableList.Builder<CharSequence> entryValues = new ImmutableList.Builder<>();
+        entries.add(getString(R.string.no_account_deactivated));
+        entryValues.add("none");
+        entries.addAll(accounts);
+        entryValues.addAll(accounts);
+        listPreference.setEntries(entries.build().toArray(new CharSequence[0]));
+        listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0]));
+        if (!accounts.contains(listPreference.getValue())) {
+            listPreference.setValue("none");
+        }
+    }
+
+    @Override
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        super.onSharedPreferenceChanged(key);
+        if (UnifiedPushDistributor.PREFERENCES.contains(key)) {
+            final var service = requireService();
+            if (service.reconfigurePushDistributor()) {
+                service.renewUnifiedPushEndpoints();
+            }
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.unified_push_distributor);
+    }
+}

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

@@ -0,0 +1,96 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.Preference;
+
+import com.rarepebble.colorpicker.ColorPreference;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.XmppActivity;
+
+public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
+
+    private final SharedPreferences.OnSharedPreferenceChangeListener
+            sharedPreferenceChangeListener =
+                    (sharedPreferences, key) -> {
+                        if (key == null) {
+                            return;
+                        }
+                        if (isAdded()) {
+                            onSharedPreferenceChanged(key);
+                        }
+                    };
+
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        Log.d(Config.LOGTAG,"onSharedPreferenceChanged("+key+")");
+    }
+
+    public void onBackendConnected() {}
+
+    @Override
+    public void onDisplayPreferenceDialog(Preference preference) {
+        if (preference instanceof ColorPreference) {
+            ((ColorPreference) preference).showDialog(this, 0);
+        } else super.onDisplayPreferenceDialog(preference);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        final var sharedPreferences = getPreferenceManager().getSharedPreferences();
+        if (sharedPreferences != null) {
+            sharedPreferences.registerOnSharedPreferenceChangeListener(
+                    this.sharedPreferenceChangeListener);
+        }
+        final var xmppActivity = requireXmppActivity();
+        if (xmppActivity.xmppConnectionService != null) {
+            this.onBackendConnected();
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        final var sharedPreferences = getPreferenceManager().getSharedPreferences();
+        if (sharedPreferences != null) {
+            sharedPreferences.unregisterOnSharedPreferenceChangeListener(
+                    this.sharedPreferenceChangeListener);
+        }
+    }
+
+    protected void reconnectAccounts() {
+        final var service = requireService();
+        for (final Account account : service.getAccounts()) {
+            if (account.isEnabled()) {
+                service.reconnectAccountInBackground(account);
+            }
+        }
+    }
+
+    protected XmppActivity requireXmppActivity() {
+        final var activity = requireActivity();
+        if (activity instanceof XmppActivity xmppActivity) {
+            return xmppActivity;
+        }
+        throw new IllegalStateException();
+    }
+
+    protected XmppConnectionService requireService() {
+        final var xmppActivity = requireXmppActivity();
+        final var service = xmppActivity.xmppConnectionService;
+        if (service != null) {
+            return service;
+        }
+        throw new IllegalStateException();
+    }
+
+    protected void runOnUiThread(final Runnable runnable) {
+        requireActivity().runOnUiThread(runnable);
+    }
+}

src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java 🔗

@@ -22,10 +22,7 @@ import android.widget.TextView;
 import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 
-import java.lang.ref.WeakReference;
-import java.util.Locale;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import com.google.common.primitives.Ints;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -34,9 +31,19 @@ import eu.siacs.conversations.services.MediaPlayer;
 import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.adapter.MessageAdapter;
 import eu.siacs.conversations.ui.util.PendingItem;
+import eu.siacs.conversations.utils.TimeFrameUtils;
 import eu.siacs.conversations.utils.WeakReferenceSet;
 
-public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable, SensorEventListener {
+import java.lang.ref.WeakReference;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class AudioPlayer
+        implements View.OnClickListener,
+                MediaPlayer.OnCompletionListener,
+                SeekBar.OnSeekBarChangeListener,
+                Runnable,
+                SensorEventListener {
 
     private static final int REFRESH_INTERVAL = 250;
     private static final Object LOCK = new Object();
@@ -57,33 +64,39 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
         final Context context = adapter.getContext();
         this.messageAdapter = adapter;
         this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
-        this.proximitySensor = this.sensorManager == null ? null : this.sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+        this.proximitySensor =
+                this.sensorManager == null
+                        ? null
+                        : this.sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
         initializeProximityWakeLock(context);
         synchronized (AudioPlayer.LOCK) {
             if (AudioPlayer.player != null) {
                 AudioPlayer.player.setOnCompletionListener(this);
                 if (AudioPlayer.player.isPlaying() && sensorManager != null) {
-                    sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+                    sensorManager.registerListener(
+                            this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
                 }
             }
         }
     }
 
-    private static String formatTime(int ms) {
-        return String.format(Locale.ENGLISH, "%d:%02d", ms / 60000, Math.min(Math.round((ms % 60000) / 1000f), 59));
+    private static String formatTime(final int ms) {
+        return TimeFrameUtils.formatElapsedTime(ms,false);
     }
 
     private void initializeProximityWakeLock(Context context) {
-        if (Build.VERSION.SDK_INT >= 21) {
-            synchronized (AudioPlayer.LOCK) {
-                if (AudioPlayer.wakeLock == null) {
-                    final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-                    AudioPlayer.wakeLock = powerManager == null ? null : powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, AudioPlayer.class.getSimpleName());
-                    AudioPlayer.wakeLock.setReferenceCounted(false);
-                }
+        synchronized (AudioPlayer.LOCK) {
+            if (AudioPlayer.wakeLock == null) {
+                final PowerManager powerManager =
+                        (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+                AudioPlayer.wakeLock =
+                        powerManager == null
+                                ? null
+                                : powerManager.newWakeLock(
+                                        PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
+                                        AudioPlayer.class.getSimpleName());
+                AudioPlayer.wakeLock.setReferenceCounted(false);
             }
-        } else {
-            AudioPlayer.wakeLock = null;
         }
     }
 
@@ -92,41 +105,39 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
             audioPlayer.setTag(message);
             if (init(ViewHolder.get(audioPlayer), message)) {
                 this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
-                executor.execute(()-> this.stopRefresher(true));
+                executor.execute(() -> this.stopRefresher(true));
             } else {
                 this.audioPlayerLayouts.removeWeakReferenceTo(audioPlayer);
             }
         }
     }
 
-    private boolean init(ViewHolder viewHolder, Message message) {
-        if (viewHolder.darkBackground) {
-            viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
-        } else {
-            viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption);
-        }
+    private boolean init(final ViewHolder viewHolder, final Message message) {
+        MessageAdapter.setTextColor(viewHolder.runtime, viewHolder.bubbleColor);
         viewHolder.progress.setOnSeekBarChangeListener(this);
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            ColorStateList color = ContextCompat.getColorStateList(messageAdapter.getContext(), viewHolder.darkBackground ? R.color.white70 : R.color.green700_desaturated);
-            viewHolder.progress.setThumbTintList(color);
-            viewHolder.progress.setProgressTintList(color);
-        }
-        viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
+        final ColorStateList color =
+                MessageAdapter.bubbleToOnSurfaceColorStateList(
+                        viewHolder.progress, viewHolder.bubbleColor);
+        viewHolder.progress.setThumbTintList(color);
+        viewHolder.progress.setProgressTintList(color);
         viewHolder.playPause.setOnClickListener(this);
         final Context context = viewHolder.playPause.getContext();
         if (message == currentlyPlayingMessage) {
             if (AudioPlayer.player != null && AudioPlayer.player.isPlaying()) {
-                viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
+                viewHolder.playPause.setImageResource(R.drawable.ic_pause_24dp);
+                MessageAdapter.setImageTint(viewHolder.playPause, viewHolder.bubbleColor);
                 viewHolder.playPause.setContentDescription(context.getString(R.string.pause_audio));
                 viewHolder.progress.setEnabled(true);
             } else {
                 viewHolder.playPause.setContentDescription(context.getString(R.string.play_audio));
-                viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
+                viewHolder.playPause.setImageResource(R.drawable.ic_play_arrow_24dp);
+                MessageAdapter.setImageTint(viewHolder.playPause, viewHolder.bubbleColor);
                 viewHolder.progress.setEnabled(false);
             }
             return true;
         } else {
-            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
+            viewHolder.playPause.setImageResource(R.drawable.ic_play_arrow_24dp);
+            MessageAdapter.setImageTint(viewHolder.playPause, viewHolder.bubbleColor);
             viewHolder.playPause.setContentDescription(context.getString(R.string.play_audio));
             viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
             viewHolder.progress.setProgress(0);
@@ -145,9 +156,16 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
     }
 
     private void startStop(ImageButton playPause) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
+                && ContextCompat.checkSelfPermission(
+                                messageAdapter.getActivity(),
+                                Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                        != PackageManager.PERMISSION_GRANTED) {
             pendingOnClickView.push(new WeakReference<>(playPause));
-            ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_PLAY_PAUSE);
+            ActivityCompat.requestPermissions(
+                    messageAdapter.getActivity(),
+                    new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
+                    ConversationsActivity.REQUEST_PLAY_PAUSE);
             return;
         }
         initializeProximityWakeLock(playPause.getContext());
@@ -163,13 +181,13 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 
     private boolean playPauseCurrent(final ViewHolder viewHolder) {
         final Context context = viewHolder.playPause.getContext();
-        viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
         if (player.isPlaying()) {
             viewHolder.progress.setEnabled(false);
             player.pause();
             messageAdapter.flagScreenOff();
             releaseProximityWakeLock();
-            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
+            viewHolder.playPause.setImageResource(R.drawable.ic_play_arrow_24dp);
+            MessageAdapter.setImageTint(viewHolder.playPause, viewHolder.bubbleColor);
             viewHolder.playPause.setContentDescription(context.getString(R.string.play_audio));
         } else {
             viewHolder.progress.setEnabled(true);
@@ -177,7 +195,8 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
             messageAdapter.flagScreenOn();
             acquireProximityWakeLock();
             this.stopRefresher(true);
-            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
+            viewHolder.playPause.setImageResource(R.drawable.ic_pause_24dp);
+            MessageAdapter.setImageTint(viewHolder.playPause, viewHolder.bubbleColor);
             viewHolder.playPause.setContentDescription(context.getString(R.string.pause_audio));
         }
         return false;
@@ -193,19 +212,24 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
         AudioPlayer.player = new MediaPlayer();
         try {
             AudioPlayer.currentlyPlayingMessage = message;
-            AudioPlayer.player.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC);
-            AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
+            AudioPlayer.player.setAudioStreamType(
+                    earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC);
+            AudioPlayer.player.setDataSource(
+                    messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
             AudioPlayer.player.setOnCompletionListener(this);
             AudioPlayer.player.prepare();
             AudioPlayer.player.start();
             messageAdapter.flagScreenOn();
             acquireProximityWakeLock();
             viewHolder.progress.setEnabled(true);
-            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
-            viewHolder.playPause.setContentDescription(viewHolder.playPause.getContext().getString(R.string.pause_audio));
-            sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+            viewHolder.playPause.setImageResource(R.drawable.ic_pause_24dp);
+            MessageAdapter.setImageTint(viewHolder.playPause, viewHolder.bubbleColor);
+            viewHolder.playPause.setContentDescription(
+                    viewHolder.playPause.getContext().getString(R.string.pause_audio));
+            sensorManager.registerListener(
+                    this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
             return true;
-        } catch (Exception e) {
+        } catch (final Exception e) {
             messageAdapter.flagScreenOff();
             releaseProximityWakeLock();
             AudioPlayer.currentlyPlayingMessage = null;
@@ -251,14 +275,16 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
         }
     }
 
-    private void resetPlayerUi(RelativeLayout audioPlayer) {
+    private void resetPlayerUi(final RelativeLayout audioPlayer) {
         if (audioPlayer == null) {
             return;
         }
         final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
         final Message message = (Message) audioPlayer.getTag();
-        viewHolder.playPause.setContentDescription(viewHolder.playPause.getContext().getString(R.string.play_audio));
-        viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
+        viewHolder.playPause.setContentDescription(
+                viewHolder.playPause.getContext().getString(R.string.play_audio));
+        viewHolder.playPause.setImageResource(R.drawable.ic_play_arrow_24dp);
+        MessageAdapter.setImageTint(viewHolder.playPause, viewHolder.bubbleColor);
         if (message != null) {
             viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
         }
@@ -298,14 +324,10 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
     }
 
     @Override
-    public void onStartTrackingTouch(SeekBar seekBar) {
-
-    }
+    public void onStartTrackingTouch(SeekBar seekBar) {}
 
     @Override
-    public void onStopTrackingTouch(SeekBar seekBar) {
-
-    }
+    public void onStopTrackingTouch(SeekBar seekBar) {}
 
     public void stop() {
         synchronized (AudioPlayer.LOCK) {
@@ -360,9 +382,11 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
         if (duration <= 0) {
             viewHolder.progress.setProgress(100);
         } else {
-            viewHolder.progress.setProgress(current * 100 / duration);
+            final var progress = current * 100L / duration;
+            viewHolder.progress.setProgress(Math.min(Ints.saturatedCast(progress), 100));
         }
-        viewHolder.runtime.setText(String.format("%s / %s", formatTime(current), formatTime(duration)));
+        viewHolder.runtime.setText(
+                String.format("%s / %s", formatTime(current), formatTime(duration)));
         return true;
     }
 
@@ -392,7 +416,11 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
                 try {
                     ViewHolder currentViewHolder = getCurrentViewHolder();
                     if (currentViewHolder != null) {
-                        play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress);
+                        play(
+                                currentViewHolder,
+                                currentlyPlayingMessage,
+                                streamType == AudioManager.STREAM_VOICE_CALL,
+                                progress);
                     }
                 } catch (Exception e) {
                     Log.w(Config.LOGTAG, e);
@@ -402,8 +430,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
     }
 
     @Override
-    public void onAccuracyChanged(Sensor sensor, int i) {
-    }
+    public void onAccuracyChanged(Sensor sensor, int i) {}
 
     private void acquireProximityWakeLock() {
         synchronized (AudioPlayer.LOCK) {
@@ -436,22 +463,24 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
         private TextView runtime;
         private SeekBar progress;
         private ImageButton playPause;
-        private boolean darkBackground = false;
-
-        public static ViewHolder get(RelativeLayout audioPlayer) {
-            ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
-            if (viewHolder == null) {
-                viewHolder = new ViewHolder();
-                viewHolder.runtime = audioPlayer.findViewById(R.id.runtime);
-                viewHolder.progress = audioPlayer.findViewById(R.id.progress);
-                viewHolder.playPause = audioPlayer.findViewById(R.id.play_pause);
-                audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
+        private MessageAdapter.BubbleColor bubbleColor = MessageAdapter.BubbleColor.SURFACE;
+
+        public static ViewHolder get(final RelativeLayout audioPlayer) {
+            final var existingViewHolder =
+                    (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
+            if (existingViewHolder != null) {
+                return existingViewHolder;
             }
+            final ViewHolder viewHolder = new ViewHolder();
+            viewHolder.runtime = audioPlayer.findViewById(R.id.runtime);
+            viewHolder.progress = audioPlayer.findViewById(R.id.progress);
+            viewHolder.playPause = audioPlayer.findViewById(R.id.play_pause);
+            audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
             return viewHolder;
         }
 
-        public void setDarkBackground(boolean darkBackground) {
-            this.darkBackground = darkBackground;
+        public void setBubbleColor(final MessageAdapter.BubbleColor bubbleColor) {
+            this.bubbleColor = bubbleColor;
         }
     }
 }

src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java 🔗

@@ -38,6 +38,7 @@ import android.os.Build;
 import android.text.Editable;
 import android.text.Spanned;
 import android.text.style.URLSpan;
+import android.view.SoundEffectConstants;
 import android.view.View;
 import android.widget.Toast;
 
@@ -46,6 +47,7 @@ import java.util.Arrays;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.ui.ConversationsActivity;
+import eu.siacs.conversations.ui.ShowLocationActivity;
 
 @SuppressLint("ParcelCreator")
 public class FixedURLSpan extends URLSpan {
@@ -61,14 +63,18 @@ public class FixedURLSpan extends URLSpan {
 		this.account = account;
 	}
 
-	public static void fix(final Editable editable) {
-		for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) {
-			final int start = editable.getSpanStart(urlspan);
-			final int end = editable.getSpanEnd(urlspan);
-			editable.removeSpan(urlspan);
-			editable.setSpan(new FixedURLSpan(urlspan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-		}
-	}
+    public static void fix(final Editable editable) {
+        for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) {
+            final int start = editable.getSpanStart(urlspan);
+            final int end = editable.getSpanEnd(urlspan);
+            editable.removeSpan(urlspan);
+            editable.setSpan(
+                    new FixedURLSpan(urlspan.getURL()),
+                    start,
+                    end,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        }
+    }
 
 	@Override
 	public void onClick(View widget) {
@@ -77,24 +83,27 @@ public class FixedURLSpan extends URLSpan {
 		final boolean candidateToProcessDirectly = "xmpp".equals(uri.getScheme()) || ("https".equals(uri.getScheme()) && "conversations.im".equals(uri.getHost()) && uri.getPathSegments().size() > 1 && Arrays.asList("j","i").contains(uri.getPathSegments().get(0)));
 		if (candidateToProcessDirectly && context instanceof ConversationsActivity) {
 			if (((ConversationsActivity) context).onXmppUriClicked(uri)) {
-				widget.playSoundEffect(0);
+				widget.playSoundEffect(SoundEffectConstants.CLICK);
 				return;
 			}
 		}
 
 		if (("sms".equals(uri.getScheme()) || "tel".equals(uri.getScheme())) && context instanceof ConversationsActivity) {
 			if (((ConversationsActivity) context).onTelUriClicked(uri, account)) {
-				widget.playSoundEffect(0);
+				widget.playSoundEffect(SoundEffectConstants.CLICK);
 				return;
 			}
 		}
 
 		final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-		//intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+      if ("geo".equalsIgnoreCase(uri.getScheme())) {
+          intent.setClass(context, ShowLocationActivity.class);
+      } else {
+          intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+		}
 		try {
 			context.startActivity(intent);
-			widget.playSoundEffect(0);
+			widget.playSoundEffect(SoundEffectConstants.CLICK);
 		} catch (ActivityNotFoundException e) {
 			Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show();
 		}

src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java 🔗

@@ -1,88 +0,0 @@
-package eu.siacs.conversations.ui.util;
-
-import android.content.Context;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.lang.reflect.Field;
-
-public class ActionBarUtil {
-
-    public static void resetActionBarOnClickListeners(@NonNull View view) {
-        final View title = findActionBarTitle(view);
-        final View subtitle = findActionBarSubTitle(view);
-        if (title != null) {
-            title.setOnClickListener(null);
-        }
-        if (subtitle != null) {
-            subtitle.setOnClickListener(null);
-        }
-    }
-
-    public static void setActionBarOnClickListener(@NonNull View view,
-                                                   @NonNull final View.OnClickListener onClickListener) {
-        final View title = findActionBarTitle(view);
-        final View subtitle = findActionBarSubTitle(view);
-        if (title != null) {
-            title.setOnClickListener(onClickListener);
-        }
-        if (subtitle != null) {
-            subtitle.setOnClickListener(onClickListener);
-        }
-    }
-
-    private static @Nullable View findActionBarTitle(@NonNull View root) {
-        return findActionBarItem(root, "action_bar_title", "mTitleTextView");
-    }
-
-    private static @Nullable
-    View findActionBarSubTitle(@NonNull View root) {
-        return findActionBarItem(root, "action_bar_subtitle", "mSubtitleTextView");
-    }
-
-    private static @Nullable View findActionBarItem(@NonNull View root,
-                                                    @NonNull String resourceName,
-                                                    @NonNull String toolbarFieldName) {
-        View result = findViewSupportOrAndroid(root, resourceName);
-
-        if (result == null) {
-            View actionBar = findViewSupportOrAndroid(root, "action_bar");
-            if (actionBar != null) {
-                result = reflectiveRead(actionBar, toolbarFieldName);
-            }
-        }
-        if (result == null && root.getClass().getName().endsWith("widget.Toolbar")) {
-            result = reflectiveRead(root, toolbarFieldName);
-        }
-        return result;
-    }
-
-    @SuppressWarnings("ConstantConditions")
-    private static @Nullable View findViewSupportOrAndroid(@NonNull View root,
-                                                           @NonNull String resourceName) {
-        Context context = root.getContext();
-        View result = null;
-        if (result == null) {
-            int supportID = context.getResources().getIdentifier(resourceName, "id", context.getPackageName());
-            result = root.findViewById(supportID);
-        }
-        if (result == null) {
-            int androidID = context.getResources().getIdentifier(resourceName, "id", "android");
-            result = root.findViewById(androidID);
-        }
-        return result;
-    }
-
-    @SuppressWarnings("unchecked")
-    private static <T> T reflectiveRead(@NonNull Object object, @NonNull String fieldName) {
-        try {
-            Field field = object.getClass().getDeclaredField(fieldName);
-            field.setAccessible(true);
-            return (T) field.get(object);
-        } catch (final Exception ex) {
-            return null;
-        }
-    }
-}

src/main/java/eu/siacs/conversations/ui/util/Attachment.java 🔗

@@ -36,9 +36,12 @@ import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import androidx.annotation.NonNull;
+
 import com.google.common.base.MoreObjects;
 
-import org.jetbrains.annotations.NotNull;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.utils.MimeUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -46,9 +49,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 
-import eu.siacs.conversations.utils.Compatibility;
-import eu.siacs.conversations.utils.MimeUtils;
-
 public class Attachment implements Parcelable {
 
     Attachment(Parcel in) {
@@ -71,17 +71,18 @@ public class Attachment implements Parcelable {
         return 0;
     }
 
-    public static final Creator<Attachment> CREATOR = new Creator<Attachment>() {
-        @Override
-        public Attachment createFromParcel(Parcel in) {
-            return new Attachment(in);
-        }
+    public static final Creator<Attachment> CREATOR =
+            new Creator<Attachment>() {
+                @Override
+                public Attachment createFromParcel(Parcel in) {
+                    return new Attachment(in);
+                }
 
-        @Override
-        public Attachment[] newArray(int size) {
-            return new Attachment[size];
-        }
-    };
+                @Override
+                public Attachment[] newArray(int size) {
+                    return new Attachment[size];
+                }
+            };
 
     public String getMime() {
         return mime;
@@ -91,7 +92,7 @@ public class Attachment implements Parcelable {
         return type;
     }
 
-    @NotNull
+    @NonNull
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
@@ -103,7 +104,10 @@ public class Attachment implements Parcelable {
     }
 
     public enum Type {
-        FILE, IMAGE, LOCATION, RECORDING
+        FILE,
+        IMAGE,
+        LOCATION,
+        RECORDING
     }
 
     private final Uri uri;
@@ -125,8 +129,8 @@ public class Attachment implements Parcelable {
         this.uuid = UUID.randomUUID();
     }
 
-    public static boolean canBeSendInband(final List<Attachment> attachments) {
-        for (Attachment attachment : attachments) {
+    public static boolean canBeSendInBand(final List<Attachment> attachments) {
+        for (final Attachment attachment : attachments) {
             if (attachment.type != Type.LOCATION) {
                 return false;
             }
@@ -135,10 +139,30 @@ public class Attachment implements Parcelable {
     }
 
     public static List<Attachment> of(final Context context, Uri uri, Type type) {
-        final String mime = type == Type.LOCATION ? null : MimeUtils.guessMimeTypeFromUri(context, uri);
+        final String mime =
+                type == Type.LOCATION ? null : MimeUtils.guessMimeTypeFromUri(context, uri);
         return Collections.singletonList(new Attachment(uri, type, mime));
     }
 
+    public static Attachment of(final Message message) {
+        final UUID uuid = UUID.fromString(message.getUuid());
+        if (message.isGeoUri()) {
+            return new Attachment(uuid, Uri.EMPTY, Type.LOCATION, null);
+        }
+        final String mime = message.getMimeType();
+        if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) {
+            final Message.FileParams fileParams = message.getFileParams();
+            if (fileParams.width > 0 && fileParams.height > 0) {
+                return new Attachment(uuid, Uri.EMPTY, Type.FILE, "video/*");
+            } else if (fileParams.runtime > 0) {
+                return new Attachment(uuid, Uri.EMPTY, Type.FILE, "audio/*");
+            } else {
+                return new Attachment(uuid, Uri.EMPTY, Type.FILE, "application/octet-stream");
+            }
+        }
+        return new Attachment(uuid, Uri.EMPTY, Type.FILE, mime);
+    }
+
     public static List<Attachment> of(final Context context, List<Uri> uris, final String type) {
         final List<Attachment> attachments = new ArrayList<>();
         for (final Uri uri : uris) {
@@ -146,16 +170,25 @@ public class Attachment implements Parcelable {
                 continue;
             }
             final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type);
-            attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
+            attachments.add(
+                    new Attachment(
+                            uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
         }
         return attachments;
     }
 
     public static Attachment of(UUID uuid, final File file, String mime) {
-        return new Attachment(uuid, Uri.fromFile(file), mime != null && (isImage(mime) || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime);
+        return new Attachment(
+                uuid,
+                Uri.fromFile(file),
+                mime != null && (isImage(mime) || mime.startsWith("video/"))
+                        ? Type.IMAGE
+                        : Type.FILE,
+                mime);
     }
 
-    public static List<Attachment> extractAttachments(final Context context, final Intent intent, Type type) {
+    public static List<Attachment> extractAttachments(
+            final Context context, final Intent intent, Type type) {
         List<Attachment> uris = new ArrayList<>();
         if (intent == null) {
             return uris;
@@ -167,7 +200,8 @@ public class Attachment implements Parcelable {
             if (clipData != null) {
                 for (int i = 0; i < clipData.getItemCount(); ++i) {
                     final Uri uri = clipData.getItemAt(i).getUri();
-                    final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, contentType);
+                    final String mime =
+                            MimeUtils.guessMimeTypeFromUriAndMime(context, uri, contentType);
                     uris.add(new Attachment(uri, type, mime));
                 }
             }
@@ -179,13 +213,12 @@ public class Attachment implements Parcelable {
     }
 
     public boolean renderThumbnail() {
-        return type == Type.IMAGE || (type == Type.FILE && mime != null && renderFileThumbnail(mime));
+        return type == Type.IMAGE
+                || (type == Type.FILE && mime != null && renderFileThumbnail(mime));
     }
 
     private static boolean renderFileThumbnail(final String mime) {
-        return mime.startsWith("video/")
-                || isImage(mime)
-                || "application/pdf".equals(mime);
+        return mime.startsWith("video/") || isImage(mime) || "application/pdf".equals(mime);
     }
 
     public Uri getUri() {

src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java 🔗

@@ -100,12 +100,12 @@ public class ConversationMenuConfigurator {
 			return;
 		}
 
-		menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
+		menuSecure.setIcon(R.drawable.ic_lock_24dp);
 
 		pgp.setVisible(Config.supportOpenPgp());
 		none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
 		axolotl.setVisible(Config.supportOmemo());
-		switch (conversation.getNextEncryption()) {
+		switch (next) {
 			case Message.ENCRYPTION_PGP:
 				//menuSecure.setTitle(R.string.encrypted_with_openpgp);
 				pgp.setChecked(true);

src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java 🔗

@@ -17,6 +17,8 @@ import androidx.databinding.DataBindingUtil;
 
 import java.util.ArrayList;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.DialogQuickeditBinding;
@@ -287,6 +289,7 @@ public final class MucDetailsContextMenuHelper {
                 activity.privateMsgInMuc(conversation, user.getName());
                 return true;
             case R.id.invite:
+                // TODO use direct invites for public conferences
                 if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
                     activity.xmppConnectionService.directInvite(conversation, jid.asBareJid());
                 } else {
@@ -306,7 +309,7 @@ public final class MucDetailsContextMenuHelper {
                 activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE);
             }
         } else {
-            AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
             builder.setTitle(R.string.ban_from_conference);
             String jid = user.getRealJid().asBareJid().toString();
             SpannableString message = new SpannableString(activity.getString(R.string.removing_from_public_conference, jid));

src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java 🔗

@@ -36,6 +36,8 @@ import android.widget.Toast;
 
 import androidx.appcompat.app.AlertDialog;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
 import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -62,7 +64,7 @@ public class PresenceSelector {
     public static void selectFullJidForDirectRtpConnection(final Activity activity, final Contact contact, final RtpCapability.Capability required, final OnFullJidSelected onFullJidSelected) {
         final String[] resources = RtpCapability.filterPresences(contact, required);
         if (resources.length < 1) {
-            Toast.makeText(activity, "No online resources to call.", Toast.LENGTH_SHORT).show();
+            Toast.makeText(activity,R.string.rtp_state_contact_offline,Toast.LENGTH_LONG).show();
         } else if (resources.length == 1) {
             onFullJidSelected.onFullJidSelected(contact.getJid().withResource(resources[0]));
         } else {
@@ -72,7 +74,7 @@ public class PresenceSelector {
 
     private static void showPresenceSelectionDialog(final Activity activity, final Contact contact, final String[] resourceArray, final OnFullJidSelected onFullJidSelected) {
         final Presences presences = contact.getPresences();
-        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
         builder.setTitle(activity.getString(R.string.choose_presence));
         Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
         final Map<String, String> resourceTypeMap = typeAndName.first;
@@ -128,8 +130,8 @@ public class PresenceSelector {
         }
     }
 
-    public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+    public static void warnMutualPresenceSubscription(final Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
         builder.setTitle(conversation.getContact().getJid().toString());
         builder.setMessage(R.string.without_mutual_presence_updates);
         builder.setNegativeButton(R.string.cancel, null);

src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java 🔗

@@ -34,99 +34,107 @@ import android.content.SharedPreferences;
 import android.content.res.TypedArray;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.content.res.Configuration;
 import android.preference.PreferenceManager;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.color.MaterialColors;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.ui.Activities;
 import eu.siacs.conversations.ui.ConversationFragment;
 import eu.siacs.conversations.utils.UIHelper;
 
 public class SendButtonTool {
 
-	public static SendButtonAction getAction(final Activity activity, final Conversation c, final String text, final String subject) {
-		if (activity == null) {
-			return SendButtonAction.TEXT;
-		}
-		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
-		final boolean empty = text.length() == 0;
-		final boolean conference = c.getMode() == Conversation.MODE_MULTI;
-		if (c.getCorrectingMessage() != null && (empty || (text.equals(c.getCorrectingMessage().getBody()) && (subject.equals(c.getCorrectingMessage().getSubject())) && (c.getThread() == c.getCorrectingMessage().getThread() || (c.getThread() != null && c.getThread().equals(c.getCorrectingMessage().getThread())))))) {
-			return SendButtonAction.CANCEL;
-		} else if (conference && !c.getAccount().httpUploadAvailable()) {
-			if (empty && c.getNextCounterpart() != null) {
-				return SendButtonAction.CANCEL;
-			} else {
-				return SendButtonAction.TEXT;
-			}
-		} else {
-			if (empty && (c.getThread() == null || subject.length() == 0)) {
-				if (conference && c.getNextCounterpart() != null) {
-					return SendButtonAction.CANCEL;
-				} else {
-					String setting = preferences.getString("quick_action", activity.getResources().getString(R.string.quick_action));
-					if (!"none".equals(setting) && UIHelper.receivedLocationQuestion(c.getLatestMessage())) {
-						return SendButtonAction.SEND_LOCATION;
-					} else {
-						if ("recent".equals(setting)) {
-							setting = preferences.getString(ConversationFragment.RECENTLY_USED_QUICK_ACTION, SendButtonAction.TEXT.toString());
-							return SendButtonAction.valueOfOrDefault(setting);
-						} else {
-							return SendButtonAction.valueOfOrDefault(setting);
-						}
-					}
-				}
-			} else {
-				return SendButtonAction.TEXT;
-			}
-		}
-	}
+    public static SendButtonAction getAction(
+            final Activity activity, final Conversation c, final String text, final String subject) {
+        if (activity == null) {
+            return SendButtonAction.TEXT;
+        }
+        final SharedPreferences preferences =
+                PreferenceManager.getDefaultSharedPreferences(activity);
+        final boolean empty = text.isEmpty();
+        final boolean conference = c.getMode() == Conversation.MODE_MULTI;
+        if (c.getCorrectingMessage() != null
+		          && (empty || (text.equals(c.getCorrectingMessage().getBody()) && (subject.equals(c.getCorrectingMessage().getSubject())) && (c.getThread() == c.getCorrectingMessage().getThread() || (c.getThread() != null && c.getThread().equals(c.getCorrectingMessage().getThread())))))) {
+            return SendButtonAction.CANCEL;
+        } else if (conference && !c.getAccount().httpUploadAvailable()) {
+            if (empty && c.getNextCounterpart() != null) {
+                return SendButtonAction.CANCEL;
+            } else {
+                return SendButtonAction.TEXT;
+            }
+        } else {
+			   if (empty && (c.getThread() == null || subject.length() == 0)) {
+                if (conference && c.getNextCounterpart() != null) {
+                    return SendButtonAction.CANCEL;
+                } else {
+                    String setting =
+                            preferences.getString(
+                                    "quick_action",
+                                    activity.getResources().getString(R.string.quick_action));
+                    if (!"none".equals(setting)
+                            && UIHelper.receivedLocationQuestion(c.getLatestMessage())) {
+                        return SendButtonAction.SEND_LOCATION;
+                    } else {
+                        if ("recent".equals(setting)) {
+                            setting =
+                                    preferences.getString(
+                                            ConversationFragment.RECENTLY_USED_QUICK_ACTION,
+                                            SendButtonAction.TEXT.toString());
+                            return SendButtonAction.valueOfOrDefault(setting);
+                        } else {
+                            return SendButtonAction.valueOfOrDefault(setting);
+                        }
+                    }
+                }
+            } else {
+                return SendButtonAction.TEXT;
+            }
+        }
+    }
 
-	public static int colorForStatus(Activity activity, Presence.Status status) {
-		switch (status) {
-		case CHAT:
-		case ONLINE:
-			return 0xff4ab04a;
-		case AWAY:
-			return 0xfff69c44;
-		case XA:
-		case DND:
-			return 0xffe7524a;
-		default:
-			return StyledAttributes.getColor(activity, R.attr.icon_tint);
-		}
-	}
+    public @DrawableRes static int getSendButtonImageResource(final SendButtonAction action, final boolean canSend) {
+        return switch (action) {
+            case TEXT -> canSend ? R.drawable.ic_send_24dp : R.drawable.ic_attach_file_24dp;
+            case TAKE_PHOTO -> R.drawable.ic_camera_alt_24dp;
+            case SEND_LOCATION -> R.drawable.ic_location_pin_24dp;
+            case CHOOSE_PICTURE -> R.drawable.ic_image_24dp;
+            case RECORD_VIDEO -> R.drawable.ic_videocam_24dp;
+            case RECORD_VOICE -> R.drawable.ic_mic_24dp;
+            case CANCEL -> R.drawable.ic_cancel_24dp;
+        };
+    }
 
-	public static Drawable getSendButtonImageResource(Activity activity, SendButtonAction action, Presence.Status status, boolean canSend) {
-		final Drawable d;
-		switch (action) {
-			case TEXT:
-				d = canSend ?
-					activity.getResources().getDrawable(R.drawable.ic_send_text_online) :
-					activity.getResources().getDrawable(R.drawable.ic_attach_file_white_24dp);
-				break;
-			case RECORD_VIDEO:
-				d = activity.getResources().getDrawable(R.drawable.ic_send_videocam_online);
-				break;
-			case TAKE_PHOTO:
-				d = activity.getResources().getDrawable(R.drawable.ic_send_photo_online);
-				break;
-			case RECORD_VOICE:
-				d = activity.getResources().getDrawable(R.drawable.ic_send_voice_online);
-				break;
-			case SEND_LOCATION:
-				d = activity.getResources().getDrawable(R.drawable.ic_send_location_online);
-				break;
-			case CANCEL:
-				d = activity.getResources().getDrawable(R.drawable.ic_send_cancel_online);
-				break;
-			case CHOOSE_PICTURE:
-				d = activity.getResources().getDrawable(R.drawable.ic_send_picture_online);
-				break;
-			default:
-				return null;
-		}
-		d.mutate().setColorFilter(colorForStatus(activity, status), PorterDuff.Mode.SRC_IN);
-		return d;
-	}
+    public @ColorInt static int getSendButtonColor(final View view, final Presence.Status status) {
+        final boolean nightMode = Activities.isNightMode(view.getContext());
+        return switch (status) {
+            case OFFLINE -> MaterialColors.getColor(
+                    view, com.google.android.material.R.attr.colorOnSurface);
+            case ONLINE, CHAT -> MaterialColors.harmonizeWithPrimary(
+                    view.getContext(),
+                    ContextCompat.getColor(
+                            view.getContext(), nightMode ? R.color.green_300 : R.color.green_800));
+            case AWAY -> MaterialColors.harmonizeWithPrimary(
+                    view.getContext(),
+                    ContextCompat.getColor(
+                            view.getContext(), nightMode ? R.color.amber_300 : R.color.amber_800));
+            case XA -> MaterialColors.harmonizeWithPrimary(
+                    view.getContext(),
+                    ContextCompat.getColor(
+                            view.getContext(),
+                            nightMode ? R.color.orange_300 : R.color.orange_800));
+            case DND -> MaterialColors.harmonizeWithPrimary(
+                    view.getContext(),
+                    ContextCompat.getColor(
+                            view.getContext(), nightMode ? R.color.red_300 : R.color.red_800));
+        };
+    }
 }

src/main/java/eu/siacs/conversations/ui/util/SettingsUtils.java 🔗

@@ -1,20 +1,19 @@
 package eu.siacs.conversations.ui.util;
 
 import android.app.Activity;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
 import android.view.Window;
 import android.view.WindowManager;
 
+import eu.siacs.conversations.AppSettings;
+
 public class SettingsUtils {
-    public static void applyScreenshotPreventionSetting(Activity activity){
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
-        boolean preventScreenshots = preferences.getBoolean("prevent_screenshots", false);
-        Window activityWindow = activity.getWindow();
-        if(preventScreenshots){
-            activityWindow.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        } else {
+    public static void applyScreenshotSetting(final Activity activity) {
+        final var appSettings = new AppSettings(activity);
+        final Window activityWindow = activity.getWindow();
+        if (appSettings.isAllowScreenshots()) {
             activityWindow.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
+        } else {
+            activityWindow.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
         }
     }
 }

src/main/java/eu/siacs/conversations/ui/util/StyledAttributes.java 🔗

@@ -1,59 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.ui.util;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-
-import androidx.annotation.AttrRes;
-import androidx.annotation.ColorInt;
-
-public class StyledAttributes {
-	public static android.graphics.drawable.Drawable getDrawable(Context context, @AttrRes int id) {
-		TypedArray typedArray = context.obtainStyledAttributes(new int[]{id});
-		android.graphics.drawable.Drawable drawable = typedArray.getDrawable(0);
-		typedArray.recycle();
-		return drawable;
-	}
-
-	public static float getFloat(Context context, @AttrRes int id) {
-		TypedArray typedArray = context.obtainStyledAttributes(new int[]{id});
-		float value = typedArray.getFloat(0,0f);
-		typedArray.recycle();
-		return value;
-	}
-
-	public static @ColorInt int getColor(Context context, @AttrRes int attr) {
-		TypedArray typedArray = context.obtainStyledAttributes(new int[]{attr});
-		int color = typedArray.getColor(0,0);
-		typedArray.recycle();
-		return color;
-	}
-}

src/main/java/eu/siacs/conversations/ui/util/ToolbarUtils.java 🔗

@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.siacs.conversations.ui.util;
+
+import static java.util.Collections.max;
+import static java.util.Collections.min;
+
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.ActionMenuView;
+import androidx.appcompat.widget.Toolbar;
+
+import com.google.android.material.appbar.MaterialToolbar;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public class ToolbarUtils {
+
+    private static final Comparator<View> VIEW_TOP_COMPARATOR =
+            new Comparator<View>() {
+                @Override
+                public int compare(View view1, View view2) {
+                    return view1.getTop() - view2.getTop();
+                }
+            };
+
+    private ToolbarUtils() {
+        // Private constructor to prevent unwanted construction.
+    }
+
+    public static void resetActionBarOnClickListeners(@NonNull MaterialToolbar view) {
+        final TextView title = getTitleTextView(view);
+        final TextView subtitle = getSubtitleTextView(view);
+        if (title != null) {
+            title.setOnClickListener(null);
+        }
+        if (subtitle != null) {
+            subtitle.setOnClickListener(null);
+        }
+    }
+
+    public static void setActionBarOnClickListener(
+            @NonNull MaterialToolbar view, @NonNull final View.OnClickListener onClickListener) {
+        final TextView title = getTitleTextView(view);
+        final TextView subtitle = getSubtitleTextView(view);
+        if (title != null) {
+            title.setOnClickListener(onClickListener);
+        }
+        if (subtitle != null) {
+            subtitle.setOnClickListener(onClickListener);
+        }
+    }
+
+    @Nullable
+    public static TextView getTitleTextView(@NonNull Toolbar toolbar) {
+        List<TextView> textViews = getTextViewsWithText(toolbar, toolbar.getTitle());
+        return textViews.isEmpty() ? null : min(textViews, VIEW_TOP_COMPARATOR);
+    }
+
+    @Nullable
+    public static TextView getSubtitleTextView(@NonNull Toolbar toolbar) {
+        List<TextView> textViews = getTextViewsWithText(toolbar, toolbar.getSubtitle());
+        return textViews.isEmpty() ? null : max(textViews, VIEW_TOP_COMPARATOR);
+    }
+
+    private static List<TextView> getTextViewsWithText(
+            @NonNull Toolbar toolbar, CharSequence text) {
+        List<TextView> textViews = new ArrayList<>();
+        for (int i = 0; i < toolbar.getChildCount(); i++) {
+            View child = toolbar.getChildAt(i);
+            if (child instanceof TextView textView) {
+                if (TextUtils.equals(textView.getText(), text)) {
+                    textViews.add(textView);
+                }
+            }
+        }
+        return textViews;
+    }
+
+    @Nullable
+    public static ImageView getLogoImageView(@NonNull Toolbar toolbar) {
+        return getImageView(toolbar, toolbar.getLogo());
+    }
+
+    @Nullable
+    private static ImageView getImageView(@NonNull Toolbar toolbar, @Nullable Drawable content) {
+        if (content == null) {
+            return null;
+        }
+        for (int i = 0; i < toolbar.getChildCount(); i++) {
+            View child = toolbar.getChildAt(i);
+            if (child instanceof ImageView imageView) {
+                Drawable drawable = imageView.getDrawable();
+                if (drawable != null
+                        && drawable.getConstantState() != null
+                        && drawable.getConstantState().equals(content.getConstantState())) {
+                    return imageView;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    public static View getSecondaryActionMenuItemView(@NonNull Toolbar toolbar) {
+        ActionMenuView actionMenuView = getActionMenuView(toolbar);
+        if (actionMenuView != null) {
+            // Only return the first child of the ActionMenuView if there is more than one child
+            if (actionMenuView.getChildCount() > 1) {
+                return actionMenuView.getChildAt(0);
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    public static ActionMenuView getActionMenuView(@NonNull Toolbar toolbar) {
+        for (int i = 0; i < toolbar.getChildCount(); i++) {
+            View child = toolbar.getChildAt(i);
+            if (child instanceof ActionMenuView) {
+                return (ActionMenuView) child;
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    public static ImageButton getNavigationIconButton(@NonNull Toolbar toolbar) {
+        Drawable navigationIcon = toolbar.getNavigationIcon();
+        if (navigationIcon == null) {
+            return null;
+        }
+        for (int i = 0; i < toolbar.getChildCount(); i++) {
+            View child = toolbar.getChildAt(i);
+            if (child instanceof ImageButton imageButton) {
+                if (imageButton.getDrawable() == navigationIcon) {
+                    return imageButton;
+                }
+            }
+        }
+        return null;
+    }
+}

src/main/java/eu/siacs/conversations/ui/util/UriHelper.java 🔗

@@ -1,6 +1,8 @@
 package eu.siacs.conversations.ui.util;
 
-import java.util.HashMap;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
 
 /**
  * Helper methods for parsing URI's.
@@ -12,19 +14,18 @@ public final class UriHelper {
 	 * @param q The query string to split.
 	 * @return A hashmap containing the key-value pairs from the query string.
 	 */
-	public static HashMap<String, String> parseQueryString(final String q) {
+	public static Map<String, String> parseQueryString(final String q) {
 		if (q == null || q.isEmpty()) {
-			return null;
+            return ImmutableMap.of();
 		}
+		final ImmutableMap.Builder<String,String> queryMapBuilder = new ImmutableMap.Builder<>();
 
 		final String[] query = q.split("&");
-		// TODO: Look up the HashMap implementation and figure out what the load factor is and make sure we're not reallocating here.
-		final HashMap<String, String> queryMap = new HashMap<>(query.length);
 		for (final String param : query) {
 			final String[] pair = param.split("=");
-			queryMap.put(pair[0], pair.length == 2 && !pair[1].isEmpty() ? pair[1] : null);
+			queryMapBuilder.put(pair[0], pair.length == 2 && !pair[1].isEmpty() ? pair[1] : null);
 		}
 
-		return queryMap;
+		return queryMapBuilder.build();
 	}
 }

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

@@ -32,7 +32,9 @@ package eu.siacs.conversations.ui.widget;
 import android.content.Context;
 import android.util.AttributeSet;
 
-public class ImmediateAutoCompleteTextView extends androidx.appcompat.widget.AppCompatAutoCompleteTextView {
+import com.google.android.material.textfield.MaterialAutoCompleteTextView;
+
+public class ImmediateAutoCompleteTextView extends MaterialAutoCompleteTextView {
 
 	public ImmediateAutoCompleteTextView(Context context, AttributeSet attrs) {
 		super(context, attrs);

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

@@ -29,6 +29,8 @@ import android.graphics.RectF;
 import android.util.AttributeSet;
 import android.view.View;
 
+import androidx.core.content.ContextCompat;
+
 import com.google.zxing.ResultPoint;
 
 import java.util.HashMap;
@@ -58,25 +60,24 @@ public class ScannerView extends View {
 
     public ScannerView(final Context context, final AttributeSet attrs) {
         super(context, attrs);
-
-        final Resources res = getResources();
-        maskColor = res.getColor(R.color.black54);
-        maskResultColor = res.getColor(R.color.black87);
-        laserColor = res.getColor(R.color.red500);
-        dotColor = res.getColor(R.color.orange500);
-        dotResultColor = res.getColor(R.color.scan_result_dots);
+        final Resources resources = context.getResources();
+        maskColor = ContextCompat.getColor(context, R.color.black54);
+        maskResultColor = ContextCompat.getColor(context, R.color.black87);
+        laserColor = ContextCompat.getColor(context, R.color.red_500);
+        dotColor = ContextCompat.getColor(context, R.color.orange_500);
+        dotResultColor = ContextCompat.getColor(context, R.color.green_500);
 
         maskPaint = new Paint();
         maskPaint.setStyle(Style.FILL);
 
         laserPaint = new Paint();
-        laserPaint.setStrokeWidth(res.getDimensionPixelSize(R.dimen.scan_laser_width));
+        laserPaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.scan_laser_width));
         laserPaint.setStyle(Style.STROKE);
 
         dotPaint = new Paint();
         dotPaint.setAlpha(DOT_OPACITY);
         dotPaint.setStyle(Style.STROKE);
-        dotPaint.setStrokeWidth(res.getDimension(R.dimen.scan_dot_size));
+        dotPaint.setStrokeWidth(resources.getDimension(R.dimen.scan_dot_size));
         dotPaint.setAntiAlias(true);
     }
 

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

@@ -26,8 +26,6 @@ import android.widget.ListView;
 import androidx.fragment.app.ListFragment;
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 
-import eu.siacs.conversations.ui.util.StyledAttributes;
-
 /**
  * Subclass of {@link androidx.fragment.app.ListFragment} which provides automatic support for
  * providing the 'swipe-to-refresh' UX gesture by wrapping the the content view in a
@@ -56,7 +54,8 @@ public class SwipeRefreshListFragment extends ListFragment {
 
         final Context context = getActivity();
         if (context != null) {
-            mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, androidx.appcompat.R.attr.colorAccent));
+            // TODO are default colors fine here?
+            //mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, androidx.appcompat.R.attr.colorAccent));
         }
 
         if (onRefreshListener != null) {

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

@@ -9,6 +9,7 @@ import android.graphics.Typeface;
 import android.util.AttributeSet;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.core.content.ContextCompat;
 
 import eu.siacs.conversations.R;
@@ -18,6 +19,7 @@ public class UnreadCountCustomView extends View {
     private int unreadCount;
     private Paint paint, textPaint;
     private int backgroundColor = 0xff326130;
+    private int textColor = Color.WHITE;
 
     public UnreadCountCustomView(Context context) {
         super(context);
@@ -38,7 +40,8 @@ public class UnreadCountCustomView extends View {
 
     private void initXMLAttrs(Context context, AttributeSet attrs) {
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UnreadCountCustomView);
-        setBackgroundColor(a.getColor(a.getIndex(0), ContextCompat.getColor(context, R.color.perpy)));
+        setBackgroundColor(a.getColor(a.getIndex(0), ContextCompat.getColor(context, R.color.md_theme_light_tertiaryContainer)));
+        this.textColor = a.getColor(a.getIndex(1),ContextCompat.getColor(context, R.color.md_theme_light_onTertiaryContainer));
         a.recycle();
     }
 
@@ -47,14 +50,14 @@ public class UnreadCountCustomView extends View {
         paint.setColor(backgroundColor);
         paint.setAntiAlias(true);
         textPaint = new Paint();
-        textPaint.setColor(Color.WHITE);
+        textPaint.setColor(textColor);
         textPaint.setTextAlign(Paint.Align.CENTER);
         textPaint.setAntiAlias(true);
         textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
     }
 
     @Override
-    protected void onDraw(Canvas canvas) {
+    protected void onDraw(@NonNull Canvas canvas) {
         super.onDraw(canvas);
         float midx = canvas.getWidth() / 2.0f;
         float midy = canvas.getHeight() / 2.0f;

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

@@ -61,11 +61,7 @@ public class AccountUtils {
         final ArrayList<String> accounts = new ArrayList<>();
         for (final Account account : service.getAccounts()) {
             if (account.isEnabled()) {
-                if (Config.DOMAIN_LOCK != null) {
-                    accounts.add(account.getJid().getEscapedLocal());
-                } else {
-                    accounts.add(account.getJid().asBareJid().toEscapedString());
-                }
+                accounts.add(account.getJid().asBareJid().toEscapedString());
             }
         }
         return accounts;

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

@@ -22,10 +22,9 @@ import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.core.content.ContextCompat;
 
+import eu.siacs.conversations.AppSettings;
 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;
@@ -33,21 +32,9 @@ import java.util.List;
 
 public class Compatibility {
 
-    private static final List<String> UNUSED_SETTINGS_POST_TWENTYSIX =
-            Arrays.asList(
-                    "led",
-                    "notification_ringtone",
-                    "notification_headsup",
-                    "vibrate_on_notification");
-    private static final List<String> UNUSED_SETTINGS_PRE_TWENTYSIX =
-            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
-                || ContextCompat.checkSelfPermission(
-                                context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
-                        == PackageManager.PERMISSION_GRANTED;
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || ContextCompat.checkSelfPermission(
+                context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
     }
 
     public static boolean s() {
@@ -79,7 +66,7 @@ public class Compatibility {
             final PackageManager packageManager = context.getPackageManager();
             final ApplicationInfo applicationInfo =
                     packageManager.getApplicationInfo(context.getPackageName(), 0);
-            return applicationInfo == null || applicationInfo.targetSdkVersion >= 26;
+            return applicationInfo.targetSdkVersion >= 26;
         } catch (PackageManager.NameNotFoundException | RuntimeException e) {
             return true; // when in doubt…
         }
@@ -90,7 +77,7 @@ public class Compatibility {
             final PackageManager packageManager = context.getPackageManager();
             final ApplicationInfo applicationInfo =
                     packageManager.getApplicationInfo(context.getPackageName(), 0);
-            return applicationInfo == null || applicationInfo.targetSdkVersion >= 24;
+            return applicationInfo.targetSdkVersion >= 24;
         } catch (PackageManager.NameNotFoundException | RuntimeException e) {
             return true; // when in doubt…
         }
@@ -108,56 +95,10 @@ public class Compatibility {
         return runsAndTargetsTwentySix(context)
                 || getBooleanPreference(
                         context,
-                        SettingsActivity.KEEP_FOREGROUND_SERVICE,
+                        AppSettings.KEEP_FOREGROUND_SERVICE,
                         R.bool.enable_foreground_service);
     }
 
-    public static void removeUnusedPreferences(SettingsFragment settingsFragment) {
-        List<PreferenceCategory> categories =
-                Arrays.asList(
-                        (PreferenceCategory)
-                                settingsFragment.findPreference("notification_category"),
-                        (PreferenceCategory) settingsFragment.findPreference("advanced"));
-        for (String key :
-                (runsTwentySix()
-                        ? UNUSED_SETTINGS_POST_TWENTYSIX
-                        : UNUSED_SETTINGS_PRE_TWENTYSIX)) {
-            Preference preference = settingsFragment.findPreference(key);
-            if (preference != null) {
-                for (PreferenceCategory category : categories) {
-                    if (category != null) {
-                        category.removePreference(preference);
-                    }
-                }
-            }
-        }
-        if (Compatibility.runsTwentySix()) {
-            if (targetsTwentySix(settingsFragment.getContext())) {
-                Preference preference =
-                        settingsFragment.findPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE);
-                if (preference != null) {
-                    for (PreferenceCategory category : categories) {
-                        if (category != null) {
-                            category.removePreference(preference);
-                        }
-                    }
-                }
-            }
-        }
-
-        try {
-            Class.forName("io.sentry.Sentry");
-            Preference preference = settingsFragment.findPreference("never_send");
-            if (preference != null) {
-                for (PreferenceCategory category : categories) {
-                    if (category != null) {
-                        category.removePreference(preference);
-                    }
-                }
-            }
-        } catch (final ClassNotFoundException e) { }
-    }
-
     public static void startService(Context context, Intent intent) {
         try {
             if (Compatibility.runsAndTargetsTwentySix(context)) {
@@ -193,6 +134,17 @@ public class Compatibility {
         }
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    public static boolean isActiveNetworkMetered(
+            @NonNull final ConnectivityManager connectivityManager) {
+        try {
+            return connectivityManager.isActiveNetworkMetered();
+        } catch (final RuntimeException e) {
+            // when in doubt better assume it's metered
+            return true;
+        }
+    }
+
     public static Bundle pgpStartIntentSenderOptions() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
             return ActivityOptions.makeBasic()

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

@@ -6,6 +6,8 @@ import android.os.Bundle;
 import android.util.Base64;
 import android.util.Pair;
 
+import androidx.annotation.StringRes;
+
 import org.bouncycastle.asn1.x500.X500Name;
 import org.bouncycastle.asn1.x500.style.BCStyle;
 import org.bouncycastle.asn1.x500.style.IETFUtils;
@@ -267,19 +269,15 @@ public final class CryptoHelper {
         }
     }
 
-    public static int encryptionTypeToText(int encryption) {
-        switch (encryption) {
-            case Message.ENCRYPTION_OTR:
-                return R.string.encryption_choice_otr;
-            case Message.ENCRYPTION_AXOLOTL:
-            case Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE:
-            case Message.ENCRYPTION_AXOLOTL_FAILED:
-                return R.string.encryption_choice_omemo;
-            case Message.ENCRYPTION_NONE:
-                return R.string.encryption_choice_unencrypted;
-            default:
-                return R.string.encryption_choice_pgp;
-        }
+    public static @StringRes int encryptionTypeToText(final int encryption) {
+        return switch (encryption) {
+            case Message.ENCRYPTION_OTR -> R.string.encryption_choice_otr;
+            case Message.ENCRYPTION_AXOLOTL,
+                    Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE,
+                    Message.ENCRYPTION_AXOLOTL_FAILED -> R.string.encryption_choice_omemo;
+            case Message.ENCRYPTION_PGP -> R.string.encryption_choice_pgp;
+            default -> R.string.encryption_choice_unencrypted;
+        };
     }
 
     public static boolean isPgpEncryptedUrl(String url) {

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

@@ -1,14 +1,21 @@
 package eu.siacs.conversations.utils;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.Signature;
-import android.preference.PreferenceManager;
 import android.util.Log;
 
-import androidx.appcompat.app.AlertDialog;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.XmppActivity;
 
 import java.io.BufferedReader;
 import java.io.FileInputStream;
@@ -20,27 +27,19 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.ui.XmppActivity;
-
 public class ExceptionHelper {
 
     private static final String FILENAME = "stacktrace.txt";
     private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
 
-    public static void init(Context context) {
+    public static void init(final Context context) {
         if (Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler) {
             return;
         }
         Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(context));
     }
 
-    public static boolean checkForCrash(XmppActivity activity) {
+    public static boolean checkForCrash(final XmppActivity activity) {
         try {
             Class.forName("io.sentry.Sentry");
             return false;
@@ -51,21 +50,20 @@ public class ExceptionHelper {
             if (service == null) {
                 return false;
             }
-            final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
-            boolean neverSend = preferences.getBoolean("never_send", false);
-            if (neverSend || Config.BUG_REPORTS == null) {
+            final AppSettings appSettings = new AppSettings(activity);
+            if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) {
                 return false;
             }
             final Account account = AccountUtils.getFirstEnabled(service);
             if (account == null) {
                 return false;
             }
-            FileInputStream file = activity.openFileInput(FILENAME);
-            InputStreamReader inputStreamReader = new InputStreamReader(file);
-            BufferedReader stacktrace = new BufferedReader(inputStreamReader);
+            final FileInputStream file = activity.openFileInput(FILENAME);
+            final InputStreamReader inputStreamReader = new InputStreamReader(file);
+            final BufferedReader stacktrace = new BufferedReader(inputStreamReader);
             final StringBuilder report = new StringBuilder();
-            PackageManager pm = activity.getPackageManager();
-            PackageInfo packageInfo;
+            final PackageManager pm = activity.getPackageManager();
+            final PackageInfo packageInfo;
             try {
                 packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES);
                 final String versionName = packageInfo.versionName;
@@ -78,8 +76,7 @@ public class ExceptionHelper {
                     report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n');
                 }
                 report.append('\n');
-            } catch (Exception e) {
-                e.printStackTrace();
+            } catch (final Exception e) {
                 return false;
             }
             String line;
@@ -89,7 +86,7 @@ public class ExceptionHelper {
             }
             file.close();
             activity.deleteFile(FILENAME);
-            AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
             builder.setTitle(activity.getString(R.string.crash_report_title, activity.getString(R.string.app_name)));
             builder.setMessage(activity.getString(R.string.crash_report_message, activity.getString(R.string.app_name)));
             builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> {
@@ -99,7 +96,7 @@ public class ExceptionHelper {
                 Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE);
                 service.sendMessage(message);
             });
-            builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> preferences.edit().putBoolean("never_send", true).apply());
+            builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> appSettings.setSendCrashReports(false));
             builder.create().show();
             return true;
         } catch (final IOException ignored) {

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

@@ -46,13 +46,17 @@ public class GeoHelper {
 		}
 	}
 
+	public static GeoPoint parseGeoPoint(final Uri uri) {
+		return parseGeoPoint(uri.toString());
+	}
+
 	private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException {
-		Matcher matcher = GEO_URI.matcher(body);
+		final Matcher matcher = GEO_URI.matcher(body);
 		if (!matcher.matches()) {
 			throw new IllegalArgumentException("Invalid geo uri");
 		}
-		double latitude;
-		double longitude;
+		final double latitude;
+		final double longitude;
 		try {
 			latitude = Double.parseDouble(matcher.group(1));
 			if (latitude > 90.0 || latitude < -90.0) {
@@ -62,7 +66,7 @@ public class GeoHelper {
 			if (longitude > 180.0 || longitude < -180.0) {
 				throw new IllegalArgumentException("Invalid geo uri");
 			}
-		} catch (NumberFormatException e) {
+		} catch (final NumberFormatException e) {
 			throw new IllegalArgumentException("Invalid geo uri",e);
 		}
 		return new GeoPoint(latitude, longitude);

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

@@ -40,6 +40,8 @@ import android.util.LruCache;
 
 import androidx.annotation.ColorInt;
 
+import com.google.android.material.color.MaterialColors;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -53,7 +55,6 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.ui.util.StyledAttributes;
 import eu.siacs.conversations.xmpp.Jid;
 
 public class IrregularUnicodeDetector {
@@ -76,8 +77,8 @@ public class IrregularUnicodeDetector {
 		}
 	}
 
-	public static Spannable style(Context context, Jid jid) {
-		return style(jid, StyledAttributes.getColor(context, R.attr.color_warning));
+	public static Spannable style(final Context context, Jid jid) {
+		return style(jid, MaterialColors.getColor(context, com.google.android.material.R.attr.colorError,"colorError not found"));
 	}
 
 	private static Spannable style(Jid jid, @ColorInt int color) {

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

@@ -330,6 +330,8 @@ public final class MimeUtils {
         add("image/x-xbitmap", "xbm");
         add("image/x-xpixmap", "xpm");
         add("image/x-xwindowdump", "xwd");
+        add("message/rfc822","eml");
+        add("message/rfc822","mime");
         add("model/iges", "igs");
         add("model/iges", "iges");
         add("model/mesh", "msh");

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

@@ -7,6 +7,9 @@ import androidx.annotation.RequiresApi;
 
 import com.google.common.base.Strings;
 
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+
 import org.conscrypt.Conscrypt;
 
 import java.lang.reflect.Method;
@@ -24,22 +27,19 @@ import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSocket;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-
 public class SSLSockets {
 
     public static void setSecurity(final SSLSocket sslSocket) {
         final String[] supportProtocols;
-        final Collection<String> supportedProtocols = new LinkedList<>(
-                Arrays.asList(sslSocket.getSupportedProtocols()));
+        final Collection<String> supportedProtocols =
+                new LinkedList<>(Arrays.asList(sslSocket.getSupportedProtocols()));
         supportedProtocols.remove("SSLv3");
         supportProtocols = supportedProtocols.toArray(new String[0]);
 
         sslSocket.setEnabledProtocols(supportProtocols);
 
-        final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
-                sslSocket.getSupportedCipherSuites());
+        final String[] cipherSuites =
+                CryptoHelper.getOrderedCipherSuites(sslSocket.getSupportedCipherSuites());
         if (cipherSuites.length > 0) {
             sslSocket.setEnabledCipherSuites(cipherSuites);
         }
@@ -70,7 +70,8 @@ public class SSLSockets {
         socket.setSSLParameters(parameters);
     }
 
-    private static void setApplicationProtocolReflection(final SSLSocket socket, final String protocol) {
+    private static void setApplicationProtocolReflection(
+            final SSLSocket socket, final String protocol) {
         try {
             final Method method = socket.getClass().getMethod("setAlpnProtocols", byte[].class);
             // the concatenation of 8-bit, length prefixed protocol names, just one in our case...
@@ -78,16 +79,17 @@ public class SSLSockets {
             final byte[] protocolUTF8Bytes = protocol.getBytes(StandardCharsets.UTF_8);
             final byte[] lengthPrefixedProtocols = new byte[protocolUTF8Bytes.length + 1];
             lengthPrefixedProtocols[0] = (byte) protocol.length(); // cannot be over 255 anyhow
-            System.arraycopy(protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length);
-            method.invoke(socket, new Object[]{lengthPrefixedProtocols});
+            System.arraycopy(
+                    protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length);
+            method.invoke(socket, new Object[] {lengthPrefixedProtocols});
         } catch (Throwable e) {
-            Log.e(Config.LOGTAG,"unable to set ALPN on socket",e);
+            Log.e(Config.LOGTAG, "unable to set ALPN on socket", e);
         }
     }
 
     public static void setApplicationProtocol(final SSLSocket socket, final String protocol) {
         if (Conscrypt.isConscrypt(socket)) {
-            Conscrypt.setApplicationProtocols(socket, new String[]{protocol});
+            Conscrypt.setApplicationProtocols(socket, new String[] {protocol});
         } else {
             setApplicationProtocolReflection(socket, protocol);
         }
@@ -113,9 +115,12 @@ public class SSLSockets {
     }
 
     public static Version version(final Socket socket) {
-        if (socket instanceof SSLSocket) {
-            final SSLSocket sslSocket = (SSLSocket) socket;
-            return Version.of(sslSocket.getSession().getProtocol());
+        if (socket instanceof SSLSocket sslSocket) {
+            if (Conscrypt.isConscrypt(sslSocket)) {
+                return Version.of(sslSocket.getSession().getProtocol());
+            } else {
+                return Version.TLS_UNSUPPORTED_VERSION;
+            }
         } else {
             return Version.NONE;
         }
@@ -126,22 +131,17 @@ public class SSLSockets {
         TLS_1_1,
         TLS_1_2,
         TLS_1_3,
-        UNKNOWN,
+        TLS_UNSUPPORTED_VERSION,
         NONE;
 
         private static Version of(final String protocol) {
-            switch (Strings.nullToEmpty(protocol)) {
-                case "TLSv1":
-                    return TLS_1_0;
-                case "TLSv1.1":
-                    return TLS_1_1;
-                case "TLSv1.2":
-                    return TLS_1_2;
-                case "TLSv1.3":
-                    return TLS_1_3;
-                default:
-                    return UNKNOWN;
-            }
+            return switch (Strings.nullToEmpty(protocol)) {
+                case "TLSv1" -> TLS_1_0;
+                case "TLSv1.1" -> TLS_1_1;
+                case "TLSv1.2" -> TLS_1_2;
+                case "TLSv1.3" -> TLS_1_3;
+                default -> TLS_UNSUPPORTED_VERSION;
+            };
         }
     }
 }

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

@@ -49,6 +49,8 @@ import android.widget.TextView;
 import androidx.annotation.ColorInt;
 import androidx.core.content.ContextCompat;
 
+import com.google.android.material.color.MaterialColors;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -94,10 +96,10 @@ public class StylingHelper {
 		format(editable, end, editable.length() - 1, textColor);
 	}
 
-	public static void highlight(final Context context, final Editable editable, List<String> needles, boolean dark) {
-		for (String needle : needles) {
+	public static void highlight(final TextView view, final Editable editable, final List<String> needles) {
+		for (final String needle : needles) {
 			if (!FtsUtils.isKeyword(needle)) {
-				highlight(context, editable, needle, dark);
+				highlight(view, editable, needle);
 			}
 		}
 	}
@@ -124,14 +126,14 @@ public class StylingHelper {
 		return words;
 	}
 
-	private static void highlight(final Context context, final Editable editable, String needle, boolean dark) {
+	private static void highlight(final TextView view, final Editable editable, final String needle) {
 		final int length = needle.length();
 		String string = editable.toString();
 		int start = indexOfIgnoreCase(string, needle, 0);
 		while (start != -1) {
 			int end = start + length;
-			editable.setSpan(new BackgroundColorSpan(ContextCompat.getColor(context, dark ? R.color.blue_a100 : R.color.blue_a400)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
-			editable.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, dark ? R.color.black87 : R.color.white)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+			editable.setSpan(new BackgroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimaryFixedDim)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+			editable.setSpan(new ForegroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryFixed)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
 			start = indexOfIgnoreCase(string, needle, start + length);
 		}
 
@@ -141,8 +143,7 @@ public class StylingHelper {
 		if (start == 0 && charSequence.length() + 1 == end) {
 			return charSequence;
 		}
-		if (charSequence instanceof Spannable) {
-			Spannable spannable = (Spannable) charSequence;
+		if (charSequence instanceof Spannable spannable) {
 			Spannable sub = (Spannable) spannable.subSequence(start, end);
 			for (Class<? extends ParcelableSpan> clazz : SPAN_CLASSES) {
 				ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz);
@@ -175,25 +176,14 @@ public class StylingHelper {
 		}
 	}
 
-	public static boolean isDarkText(TextView textView) {
-		int argb = textView.getCurrentTextColor();
-		return Color.red(argb) + Color.green(argb) + Color.blue(argb) == 0;
-	}
-
-	private static ParcelableSpan createSpanForStyle(ImStyleParser.Style style) {
-		switch (style.getKeyword()) {
-			case "*":
-				return new StyleSpan(Typeface.BOLD);
-			case "_":
-				return new StyleSpan(Typeface.ITALIC);
-			case "~":
-				return new StrikethroughSpan();
-			case "`":
-			case "```":
-				return new TypefaceSpan("monospace");
-			default:
-				throw new AssertionError("Unknown Style");
-		}
+	private static ParcelableSpan createSpanForStyle(final ImStyleParser.Style style) {
+        return switch (style.getKeyword()) {
+            case "*" -> new StyleSpan(Typeface.BOLD);
+            case "_" -> new StyleSpan(Typeface.ITALIC);
+            case "~" -> new StrikethroughSpan();
+            case "`", "```" -> new TypefaceSpan("monospace");
+            default -> throw new AssertionError("Unknown Style");
+        };
 	}
 
 	private static void makeKeywordOpaque(final Editable editable, int start, int end, @ColorInt int fallbackTextColor) {

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

@@ -46,46 +46,90 @@ import androidx.core.content.ContextCompat;
 
 import com.cheogram.android.ColorResourcesLoaderCreator;
 
-import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.color.MaterialColors;
 
 import java.util.HashMap;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.ui.SettingsActivity;
+import eu.siacs.conversations.Conversations;
 
 public class ThemeHelper {
 
 	public static HashMap<Integer, Integer> applyCustomColors(final Context context) {
 		HashMap<Integer, Integer> colors = new HashMap<>();
 		if (Build.VERSION.SDK_INT < 30) return colors;
+		if (!Conversations.isCustomColorsDesired(context)) return colors;
 
 		final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-		if (sharedPreferences.contains("custom_theme_primary")) colors.put(R.color.custom_theme_primary, sharedPreferences.getInt("custom_theme_primary", 0));
-		if (sharedPreferences.contains("custom_theme_primary_dark")) colors.put(R.color.custom_theme_primary_dark, sharedPreferences.getInt("custom_theme_primary_dark", 0));
-		if (sharedPreferences.contains("custom_theme_accent")) colors.put(R.color.custom_theme_accent, sharedPreferences.getInt("custom_theme_accent", 0));
+		if (sharedPreferences.contains("custom_theme_primary")) {
+			final var roles = MaterialColors.getColorRoles(sharedPreferences.getInt("custom_theme_primary", 0), true);
+			colors.put(R.color.md_theme_light_primary, roles.getAccent());
+			colors.put(R.color.md_theme_light_onPrimary, roles.getOnAccent());
+			colors.put(R.color.md_theme_light_primaryContainer, roles.getAccentContainer());
+			colors.put(R.color.md_theme_light_onPrimaryContainer, roles.getOnAccentContainer());
+		}
+		if (sharedPreferences.contains("custom_theme_primary_dark")) {
+			final var roles = MaterialColors.getColorRoles(sharedPreferences.getInt("custom_theme_primary_dark", 0), true);
+			colors.put(R.color.md_theme_light_secondary, roles.getAccent());
+			colors.put(R.color.md_theme_light_onSecondary, roles.getOnAccent());
+			colors.put(R.color.md_theme_light_secondaryContainer, roles.getAccentContainer());
+			colors.put(R.color.md_theme_light_onSecondaryContainer, roles.getOnAccentContainer());
+		}
+		if (sharedPreferences.contains("custom_theme_accent")) {
+			final var roles = MaterialColors.getColorRoles(sharedPreferences.getInt("custom_theme_accent", 0), true);
+			colors.put(R.color.md_theme_light_tertiary, roles.getAccent());
+			colors.put(R.color.md_theme_light_onTertiary, roles.getOnAccent());
+			colors.put(R.color.md_theme_light_tertiaryContainer, roles.getAccentContainer());
+			colors.put(R.color.md_theme_light_onTertiaryContainer, roles.getOnAccentContainer());
+		}
 		if (sharedPreferences.contains("custom_theme_background_primary")) {
 			int background_primary = sharedPreferences.getInt("custom_theme_background_primary", 0);
 			int alpha = (background_primary >> 24) & 0xFF;
 			int red = (background_primary >> 16) & 0xFF;
 			int green = (background_primary >> 8) & 0xFF;
 			int blue = background_primary & 0xFF;
-			colors.put(R.color.custom_theme_background_primary, background_primary);
-			colors.put(R.color.custom_theme_background_secondary, (int)((alpha << 24) | ((int)(red*.9) << 16) | ((int)(green*.9) << 8) | (int)(blue*.9)));
-			colors.put(R.color.custom_theme_background_tertiary, (int)((alpha << 24) | ((int)(red*.85) << 16) | ((int)(green*.85) << 8) | (int)(blue*.85)));
+			colors.put(R.color.md_theme_light_background, background_primary);
+			colors.put(R.color.md_theme_light_surface, background_primary);
+			//colors.put(R.color.md_theme_light_surface, (int)((alpha << 24) | ((int)(red*.9) << 16) | ((int)(green*.9) << 8) | (int)(blue*.9)));
+			colors.put(R.color.md_theme_light_surfaceVariant, (int)((alpha << 24) | ((int)(red*.85) << 16) | ((int)(green*.85) << 8) | (int)(blue*.85)));
+		}
+		if (sharedPreferences.contains("custom_dark_theme_primary")) {
+			final var base = sharedPreferences.getInt("custom_dark_theme_primary", 0);
+			final var black = base == context.getColor(android.R.color.black);
+			final var roles = MaterialColors.getColorRoles(base, false);
+			colors.put(R.color.md_theme_dark_primary, black ? base : roles.getAccent());
+			colors.put(R.color.md_theme_dark_onPrimary, black ? context.getColor(R.color.white) : roles.getOnAccent());
+			colors.put(R.color.md_theme_dark_primaryContainer, black ? base : roles.getAccentContainer());
+			colors.put(R.color.md_theme_dark_onPrimaryContainer, black ? context.getColor(R.color.white) : roles.getOnAccentContainer());
+		}
+		if (sharedPreferences.contains("custom_dark_theme_primary_dark")) {
+			final var base = sharedPreferences.getInt("custom_dark_theme_primary_dark", 0);
+			final var black = base == context.getColor(android.R.color.black);
+			final var roles = MaterialColors.getColorRoles(base, false);
+			colors.put(R.color.md_theme_dark_secondary, black ? base : roles.getAccent());
+			colors.put(R.color.md_theme_dark_onSecondary, black ? context.getColor(R.color.white) : roles.getOnAccent());
+			colors.put(R.color.md_theme_dark_secondaryContainer, black ? base : roles.getAccentContainer());
+			colors.put(R.color.md_theme_dark_onSecondaryContainer, black ? context.getColor(R.color.white) : roles.getOnAccentContainer());
+		}
+		if (sharedPreferences.contains("custom_dark_theme_accent")) {
+			final var base = sharedPreferences.getInt("custom_dark_theme_accent", 0);
+			final var black = base == context.getColor(android.R.color.black);
+			final var roles = MaterialColors.getColorRoles(base, false);
+			colors.put(R.color.md_theme_dark_tertiary, black ? base : roles.getAccent());
+			colors.put(R.color.md_theme_dark_onTertiary, black ? context.getColor(R.color.white) : roles.getOnAccent());
+			colors.put(R.color.md_theme_dark_tertiaryContainer, black ? base : roles.getAccentContainer());
+			colors.put(R.color.md_theme_dark_onTertiaryContainer, black ? context.getColor(R.color.white) : roles.getOnAccentContainer());
 		}
-		if (sharedPreferences.contains("custom_dark_theme_primary")) colors.put(R.color.custom_dark_theme_primary, sharedPreferences.getInt("custom_dark_theme_primary", 0));
-		if (sharedPreferences.contains("custom_dark_theme_primary_dark")) colors.put(R.color.custom_dark_theme_primary_dark, sharedPreferences.getInt("custom_dark_theme_primary_dark", 0));
-		if (sharedPreferences.contains("custom_dark_theme_accent")) colors.put(R.color.custom_dark_theme_accent, sharedPreferences.getInt("custom_dark_theme_accent", 0));
 		if (sharedPreferences.contains("custom_dark_theme_background_primary")) {
 			int background_primary = sharedPreferences.getInt("custom_dark_theme_background_primary", 0);
 			int alpha = (background_primary >> 24) & 0xFF;
 			int red = (background_primary >> 16) & 0xFF;
 			int green = (background_primary >> 8) & 0xFF;
 			int blue = background_primary & 0xFF;
-			colors.put(R.color.custom_dark_theme_background_primary, background_primary);
-			colors.put(R.color.custom_dark_theme_background_secondary, (int)((alpha << 24) | ((int)(red*.5) << 16) | ((int)(green*.5) << 8) | (int)(blue*.5)));
-			colors.put(R.color.custom_dark_theme_background_tertiary, (int)((alpha << 24) | ((int)(40 + red*.84) << 16) | ((int)(40 + green*.84) << 8) | (int)(40 + blue*.84)));
+			colors.put(R.color.md_theme_dark_background, background_primary);
+			colors.put(R.color.md_theme_dark_surface, background_primary);
+			colors.put(R.color.md_theme_dark_surfaceVariant, (int)((alpha << 24) | ((int)(40 + red*.84) << 16) | ((int)(40 + green*.84) << 8) | (int)(40 + blue*.84)));
 		}
 		if (colors.isEmpty()) return colors;
 
@@ -97,92 +141,4 @@ public class ThemeHelper {
 		}
 		return colors;
 	}
-
-	public static int find(final Context context) {
-		final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-		final Resources resources = context.getResources();
-		final String setting = sharedPreferences.getString(SettingsActivity.THEME, resources.getString(R.string.theme));
-		final boolean dark = isDark(sharedPreferences, resources);
-		final String fontSize = sharedPreferences.getString("font_size", resources.getString(R.string.default_font_size));
-		switch (fontSize) {
-			case "medium":
-				if ("obsidian".equals(setting)) return R.style.ConversationsTheme_Obsidian_Medium;
-				else if ("oledblack".equals(setting)) return R.style.ConversationsTheme_OLEDBlack_Medium;
-				else if ("custom".equals(setting)) return dark ? R.style.ConversationsTheme_CustomDark_Medium : R.style.ConversationsTheme_Custom_Medium;
-				return dark ? R.style.ConversationsTheme_Dark_Medium : R.style.ConversationsTheme_Medium;
-			case "large":
-				if ("obsidian".equals(setting)) return R.style.ConversationsTheme_Obsidian_Large;
-				else if ("oledblack".equals(setting)) return R.style.ConversationsTheme_OLEDBlack_Large;
-				else if ("custom".equals(setting)) return dark ? R.style.ConversationsTheme_CustomDark_Large : R.style.ConversationsTheme_Custom_Large;
-				return dark ? R.style.ConversationsTheme_Dark_Large : R.style.ConversationsTheme_Large;
-			default:
-				if ("obsidian".equals(setting)) return R.style.ConversationsTheme_Obsidian;
-				else if ("oledblack".equals(setting)) return R.style.ConversationsTheme_OLEDBlack;
-				else if ("custom".equals(setting)) return dark ? R.style.ConversationsTheme_CustomDark : R.style.ConversationsTheme_Custom;
-				return dark ? R.style.ConversationsTheme_Dark : R.style.ConversationsTheme;
-		}
-	}
-
-	public static int findDialog(Context context) {
-		final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-		final Resources resources = context.getResources();
-		final boolean dark = isDark(sharedPreferences, resources);
-		final String fontSize = sharedPreferences.getString("font_size", resources.getString(R.string.default_font_size));
-		switch (fontSize) {
-			case "medium":
-				return dark ? R.style.ConversationsTheme_Dark_Dialog_Medium : R.style.ConversationsTheme_Dialog_Medium;
-			case "large":
-				return dark ? R.style.ConversationsTheme_Dark_Dialog_Large : R.style.ConversationsTheme_Dialog_Large;
-			default:
-				return dark ? R.style.ConversationsTheme_Dark_Dialog : R.style.ConversationsTheme_Dialog;
-		}
-	}
-
-	private static boolean isDark(final SharedPreferences sharedPreferences, final Resources resources) {
-		final String setting = sharedPreferences.getString(SettingsActivity.THEME, resources.getString(R.string.theme));
-		if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && "automatic".equals(setting)) ||
-			("custom".equals(setting) && sharedPreferences.getBoolean("custom_theme_automatic", false))
-		) {
-			return (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
-		} else {
-			if ("custom".equals(setting)) return sharedPreferences.getBoolean("custom_theme_dark", false);
-			return "dark".equals(setting) || "obsidian".equals(setting) || "oledblack".equals(setting);
-		}
-	}
-
-	public static boolean isDark(@StyleRes int id) {
-		switch (id) {
-			case R.style.ConversationsTheme_Dark:
-			case R.style.ConversationsTheme_Dark_Large:
-			case R.style.ConversationsTheme_Dark_Medium:
-			case R.style.ConversationsTheme_CustomDark:
-			case R.style.ConversationsTheme_CustomDark_Large:
-			case R.style.ConversationsTheme_CustomDark_Medium:
-			case R.style.ConversationsTheme_Obsidian:
-			case R.style.ConversationsTheme_Obsidian_Large:
-			case R.style.ConversationsTheme_Obsidian_Medium:
-			case R.style.ConversationsTheme_OLEDBlack:
-			case R.style.ConversationsTheme_OLEDBlack_Large:
-			case R.style.ConversationsTheme_OLEDBlack_Medium:
-				return true;
-			default:
-				return false;
-		}
-	}
-
-	public static void fix(Snackbar snackbar) {
-		final Context context = snackbar.getContext();
-		TypedArray typedArray = context.obtainStyledAttributes(new int[]{R.attr.TextSizeBody1});
-		final float size = typedArray.getDimension(0,0f);
-		typedArray.recycle();
-		if (size != 0f) {
-			final TextView text = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text);
-			final TextView action = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_action);
-			if (text != null && action != null) {
-				text.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
-				action.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
-				action.setTextColor(ContextCompat.getColor(context, R.color.blue_a100));
-			}
-		}
-	}
 }

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

@@ -2,14 +2,20 @@ package eu.siacs.conversations.utils;
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.content.res.ColorStateList;
 import android.text.SpannableStringBuilder;
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
 import android.util.Pair;
+import android.widget.TextView;
 
 import androidx.annotation.ColorInt;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.annotation.ColorRes;
+import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
 
+import com.google.android.material.color.MaterialColors;
 import com.google.common.base.Strings;
 import com.google.common.primitives.Ints;
 
@@ -43,78 +49,6 @@ import eu.siacs.conversations.xmpp.Jid;
 
 public class UIHelper {
 
-    private static final int[] UNSAFE_COLORS = {
-            0xFFF44336, //red 500
-            0xFFE53935, //red 600
-            0xFFD32F2F, //red 700
-            0xFFC62828, //red 800
-
-            0xFFEF6C00, //orange 800
-
-            0xFFF4511E, //deep orange 600
-            0xFFE64A19, //deep orange 700
-            0xFFD84315, //deep orange 800,
-    };
-
-    private static final int[] SAFE_COLORS = {
-            0xFFE91E63, //pink 500
-            0xFFD81B60, //pink 600
-            0xFFC2185B, //pink 700
-            0xFFAD1457, //pink 800
-
-            0xFF9C27B0, //purple 500
-            0xFF8E24AA, //purple 600
-            0xFF7B1FA2, //purple 700
-            0xFF6A1B9A, //purple 800
-
-            0xFF673AB7, //deep purple 500,
-            0xFF5E35B1, //deep purple 600
-            0xFF512DA8, //deep purple 700
-            0xFF4527A0, //deep purple 800,
-
-            0xFF3F51B5, //indigo 500,
-            0xFF3949AB,//indigo 600
-            0xFF303F9F,//indigo 700
-            0xFF283593, //indigo 800
-
-            0xFF2196F3, //blue 500
-            0xFF1E88E5, //blue 600
-            0xFF1976D2, //blue 700
-            0xFF1565C0, //blue 800
-
-            0xFF03A9F4, //light blue 500
-            0xFF039BE5, //light blue 600
-            0xFF0288D1, //light blue 700
-            0xFF0277BD, //light blue 800
-
-            0xFF00BCD4, //cyan 500
-            0xFF00ACC1, //cyan 600
-            0xFF0097A7, //cyan 700
-            0xFF00838F, //cyan 800
-
-            0xFF009688, //teal 500,
-            0xFF00897B, //teal 600
-            0xFF00796B, //teal 700
-            0xFF00695C, //teal 800,
-
-            //0xFF558B2F, //light green 800
-
-            //0xFFC0CA33, //lime 600
-            0xFF9E9D24, //lime 800
-
-            0xFF795548, //brown 500,
-            //0xFF4E342E, //brown 800
-            0xFF607D8B, //blue grey 500,
-            //0xFF37474F //blue grey 800
-    };
-
-    private static final int[] COLORS;
-
-    static {
-        COLORS = Arrays.copyOf(SAFE_COLORS, SAFE_COLORS.length + UNSAFE_COLORS.length);
-        System.arraycopy(UNSAFE_COLORS, 0, COLORS, SAFE_COLORS.length, UNSAFE_COLORS.length);
-    }
-
     private static final List<String> LOCATION_QUESTIONS = Arrays.asList(
             "where are you", //en
             "where are you now", //en
@@ -243,30 +177,7 @@ public class UIHelper {
     }
 
     public static int getColorForName(String name) {
-        return getColorForName(name, false);
-    }
-
-    public static int getColorForName(String name, boolean safe) {
-        if (Config.XEP_0392) {
-            return XEP0392Helper.rgbFromNick(name);
-        }
-        if (name == null || name.isEmpty()) {
-            return 0xFF202020;
-        }
-        if (safe) {
-            return SAFE_COLORS[(int) (getLongForName(name) % SAFE_COLORS.length)];
-        } else {
-            return COLORS[(int) (getLongForName(name) % COLORS.length)];
-        }
-    }
-
-    private static long getLongForName(String name) {
-        try {
-            final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
-            return Math.abs(new BigInteger(messageDigest.digest(name.getBytes())).longValue());
-        } catch (Exception e) {
-            return 0;
-        }
+        return XEP0392Helper.rgbFromNick(name);
     }
 
     public static Pair<CharSequence, Boolean> getMessagePreview(final XmppConnectionService context, final Message message) {
@@ -336,7 +247,7 @@ public class UIHelper {
                 return new Pair<>(context.getString(R.string.x_file_offered_for_download,
                         getFileDescriptionString(context, message)), true);
             } else {
-                Drawable fallbackImg = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_attach_photo, null);
+                Drawable fallbackImg = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_photo_24dp, null);
                 fallbackImg.setBounds(0, 0, fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight());
                 SpannableStringBuilder styledBody = message.getSpannableBody(null, fallbackImg);
                 if (textColor != 0) {
@@ -574,26 +485,25 @@ public class UIHelper {
         }
     }
 
-    public static String getMessageHint(Context context, Conversation conversation) {
-        switch (conversation.getNextEncryption()) {
-            case Message.ENCRYPTION_NONE:
+    public static String getMessageHint(final Context context,final  Conversation conversation) {
+        return switch (conversation.getNextEncryption()) {
+            case Message.ENCRYPTION_NONE -> {
                 if (Config.multipleEncryptionChoices()) {
-                    return context.getString(R.string.send_message);
+                    yield context.getString(R.string.send_message);
                 } else {
-                    return context.getString(R.string.send_message_to_x, conversation.getName());
+                    yield context.getString(R.string.send_message_to_x, conversation.getName());
                 }
-            case Message.ENCRYPTION_AXOLOTL:
-                AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
+            }
+            case Message.ENCRYPTION_AXOLOTL -> {
+                final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
                 if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
-                    return context.getString(R.string.send_omemo_x509_message);
+                    yield context.getString(R.string.send_omemo_x509_message);
                 } else {
-                    return context.getString(R.string.send_omemo_message);
+                    yield context.getString(R.string.send_encrypted_message);
                 }
-            case Message.ENCRYPTION_PGP:
-                return context.getString(R.string.send_pgp_message);
-            default:
-                return "";
-        }
+            }
+            default -> context.getString(R.string.send_encrypted_message);
+        };
     }
 
     public static String getDisplayedMucCounterpart(final Jid counterpart) {
@@ -619,19 +529,38 @@ public class UIHelper {
         return LOCATION_QUESTIONS.contains(body);
     }
 
-    public static ListItem.Tag getTagForStatus(Context context, Presence.Status status) {
-        switch (status) {
-            case CHAT:
-                return new ListItem.Tag(context.getString(R.string.presence_chat), 0xff4ab04a);
-            case AWAY:
-                return new ListItem.Tag(context.getString(R.string.presence_away), 0xfff69c44);
-            case XA:
-                return new ListItem.Tag(context.getString(R.string.presence_xa), 0xffe7524a);
-            case DND:
-                return new ListItem.Tag(context.getString(R.string.presence_dnd), 0xffe7524a);
-            default:
-                return new ListItem.Tag(context.getString(R.string.presence_online), 0xff4ab04a);
-        }
+    public static void setStatus(final TextView textView, Presence.Status status) {
+        final @StringRes int text;
+        final @ColorRes int color =
+                switch (status) {
+                    case CHAT -> {
+                        text = R.string.presence_chat;
+                        yield R.color.green_800;
+                    }
+                    case ONLINE -> {
+                        text = R.string.presence_online;
+                        yield R.color.green_800;
+                    }
+                    case AWAY -> {
+                        text = R.string.presence_away;
+                        yield R.color.amber_800;
+                    }
+                    case XA -> {
+                        text = R.string.presence_xa;
+                        yield R.color.orange_800;
+                    }
+                    case DND -> {
+                        text = R.string.presence_dnd;
+                        yield R.color.red_800;
+                    }
+                    default -> throw new IllegalStateException();
+                };
+        textView.setText(text);
+        textView.setBackgroundTintList(
+                ColorStateList.valueOf(
+                        MaterialColors.harmonizeWithPrimary(
+                                textView.getContext(),
+                                ContextCompat.getColor(textView.getContext(), color))));
     }
 
     public static String filesizeToString(long size) {

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

@@ -36,15 +36,15 @@ import eu.siacs.conversations.Config;
 
 public class WakeLockHelper {
 
-	public static void acquire(PowerManager.WakeLock wakeLock) {
+	public static void acquire(final PowerManager.WakeLock wakeLock) {
 		try {
 			wakeLock.acquire(2000);
-		} catch (RuntimeException e) {
+		} catch (final RuntimeException e) {
 			Log.d(Config.LOGTAG, "unable to acquire wake lock", e);
 		}
 	}
 
-	public static void release(PowerManager.WakeLock wakeLock) {
+	public static void release(final PowerManager.WakeLock wakeLock) {
 		if (wakeLock == null) {
 			return;
 		}
@@ -52,7 +52,7 @@ public class WakeLockHelper {
 			if (wakeLock.isHeld()) {
 				wakeLock.release();
 			}
-		} catch (RuntimeException e) {
+		} catch (final RuntimeException e) {
 			Log.d(Config.LOGTAG, "unable to release wake lock", e);
 		}
 	}

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

@@ -7,7 +7,7 @@ import org.hsluv.HUSLColorConverter;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 
-class XEP0392Helper {
+public class XEP0392Helper {
 
     private static double angle(String nickname) {
         try {
@@ -20,7 +20,7 @@ class XEP0392Helper {
         }
     }
 
-    static int rgbFromNick(String name) {
+    public static int rgbFromNick(String name) {
         double[] hsluv = new double[3];
         hsluv[0] = angle(name) * 360;
         hsluv[1] = 85;

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

@@ -1,13 +1,15 @@
 package eu.siacs.conversations.utils;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 
+import eu.siacs.conversations.xml.Element;
+
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
-import eu.siacs.conversations.xml.Element;
-
 public class XmlHelper {
     public static String encodeEntities(String content) {
         content = content.replace("&", "&amp;");
@@ -28,4 +30,11 @@ public class XmlHelper {
                                 child -> child != null ? child.getName() : null);
         return Joiner.on(", ").join(features);
     }
+
+    public static String print(final Collection<Element> children) {
+        if (children == null) {
+            return null;
+        }
+        return Joiner.on("").join(Iterables.transform(children, Element::toString));
+    }
 }

src/main/java/eu/siacs/conversations/xml/Element.java 🔗

@@ -1,11 +1,11 @@
 package eu.siacs.conversations.xml;
 
+import androidx.annotation.NonNull;
+
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
 
-import org.jetbrains.annotations.NotNull;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Hashtable;
@@ -178,6 +178,25 @@ public class Element implements Node {
 		return this;
 	}
 
+	public String toString() {
+		final StringBuilder elementOutput = new StringBuilder();
+		if (childNodes.size() == 0) {
+			Tag emptyTag = Tag.empty(name);
+			emptyTag.setAttributes(this.attributes);
+			elementOutput.append(emptyTag.toString());
+		} else {
+			Tag startTag = Tag.start(name);
+			startTag.setAttributes(this.attributes);
+			elementOutput.append(startTag);
+			for (Node child : ImmutableList.copyOf(childNodes)) {
+				elementOutput.append(child.toString());
+			}
+			Tag endTag = Tag.end(name);
+			elementOutput.append(endTag);
+		}
+		return elementOutput.toString();
+	}
+
 	public Element removeAttribute(String name) {
 		this.attributes.remove(name);
 		return this;
@@ -220,26 +239,6 @@ public class Element implements Node {
 		return this.attributes;
 	}
 
-	@NotNull
-	public String toString() {
-		final StringBuilder elementOutput = new StringBuilder();
-		if (childNodes.size() == 0) {
-			Tag emptyTag = Tag.empty(name);
-			emptyTag.setAttributes(this.attributes);
-			elementOutput.append(emptyTag.toString());
-		} else {
-			Tag startTag = Tag.start(name);
-			startTag.setAttributes(this.attributes);
-			elementOutput.append(startTag);
-			for (Node child : ImmutableList.copyOf(childNodes)) {
-				elementOutput.append(child.toString());
-			}
-			Tag endTag = Tag.end(name);
-			elementOutput.append(endTag);
-		}
-		return elementOutput.toString();
-	}
-
 	public final String getName() {
 		return name;
 	}

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -24,6 +24,7 @@ public final class Namespace {
     public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls";
     public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
     public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
+    public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max";
     public static final String PUBSUB_ERROR = PUBSUB + "#errors";
     public static final String PUBSUB_OWNER = PUBSUB + "#owner";
     public static final String NICK = "http://jabber.org/protocol/nick";
@@ -77,4 +78,6 @@ public final class Namespace {
     public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
     public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
     public static final String HASHES = "urn:xmpp:hashes:2";
+    public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0";
+    public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
 }

src/main/java/eu/siacs/conversations/xml/Tag.java 🔗

@@ -1,6 +1,6 @@
 package eu.siacs.conversations.xml;
 
-import org.jetbrains.annotations.NotNull;
+import androidx.annotation.NonNull;
 
 import java.util.Hashtable;
 import java.util.Map.Entry;
@@ -80,7 +80,7 @@ public class Tag {
         return (this.type == NO);
     }
 
-    @NotNull
+    @NonNull
     public String toString() {
         final StringBuilder tagOutput = new StringBuilder();
         tagOutput.append('<');

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

@@ -69,6 +69,7 @@ import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509KeyManager;
 import javax.net.ssl.X509TrustManager;
 
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.XmppDomainVerifier;
@@ -204,6 +205,7 @@ public class XmppConnection implements Runnable {
             new Hashtable<>();
     private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
             new HashSet<>();
+    private final AppSettings appSettings;
     private final XmppConnectionService mXmppConnectionService;
     private Socket socket;
     private XmlReader tagReader;
@@ -212,6 +214,7 @@ public class XmppConnection implements Runnable {
     private boolean inSmacksSession = false;
     private boolean quickStartInProgress = false;
     private boolean isBound = false;
+    private boolean offlineMessagesRetrieved = false;
     private Element streamFeatures;
     private Element boundStreamFeatures;
     private StreamId streamId = null;
@@ -251,9 +254,10 @@ public class XmppConnection implements Runnable {
     public XmppConnection(final Account account, final XmppConnectionService service) {
         this.account = account;
         this.mXmppConnectionService = service;
+        this.appSettings = new AppSettings(mXmppConnectionService.getApplicationContext());
     }
 
-    private static void fixResource(Context context, Account account) {
+    private static void fixResource(final Context context, final Account account) {
         String resource = account.getResource();
         int fixedPartLength =
                 context.getString(R.string.app_name).length() + 1; // include the trailing dot
@@ -1201,7 +1205,12 @@ public class XmppConnection implements Runnable {
                 mXmppConnectionService.updateConversationUi();
             }
         } else {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + ": resumption failed ("
+                            + XmlHelper.print(failed.getChildren())
+                            + ")");
         }
         resetStreamId();
         if (sendBindRequest) {
@@ -1712,6 +1721,7 @@ public class XmppConnection implements Runnable {
                             + mechanisms);
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
+        validateRequireChannelBinding(saslMechanism);
         if (SaslMechanism.hashedToken(saslMechanism)) {
             return;
         }
@@ -1730,6 +1740,17 @@ public class XmppConnection implements Runnable {
         }
     }
 
+    private void validateRequireChannelBinding(@NonNull final SaslMechanism mechanism)
+            throws StateChangingException {
+        if (appSettings.isRequireChannelBinding()) {
+            if (mechanism instanceof ChannelBindingMechanism) {
+                return;
+            }
+            Log.d(Config.LOGTAG, account.getJid() + ": server did not offer channel binding");
+            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+        }
+    }
+
     private Element generateAuthenticationRequest(
             final String firstMessage, final boolean usingFast) {
         return generateAuthenticationRequest(
@@ -2276,6 +2297,7 @@ public class XmppConnection implements Runnable {
     }
 
     private void finalizeBind() {
+        this.offlineMessagesRetrieved = false;
         if (bindListener != null) {
             bindListener.onBind(account);
         }
@@ -2432,7 +2454,10 @@ public class XmppConnection implements Runnable {
         final SaslMechanism quickStartMechanism;
         if (secureConnection) {
             quickStartMechanism =
-                    SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
+                    SaslMechanism.ensureAvailable(
+                            account.getQuickStartMechanism(),
+                            sslVersion,
+                            appSettings.isRequireChannelBinding());
         } else {
             quickStartMechanism = null;
         }
@@ -2821,6 +2846,28 @@ public class XmppConnection implements Runnable {
         return mXmppConnectionService.getIqGenerator();
     }
 
+    public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
+        if (trackOfflineMessageRetrieval) {
+            final IqPacket iqPing = new IqPacket(IqPacket.TYPE.GET);
+            iqPing.addChild("ping", Namespace.PING);
+            this.sendIqPacket(
+                    iqPing,
+                    (a, response) -> {
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": got ping response after sending initial presence");
+                        XmppConnection.this.offlineMessagesRetrieved = true;
+                    });
+        } else {
+            this.offlineMessagesRetrieved = true;
+        }
+    }
+
+    public boolean isOfflineMessagesRetrieved() {
+        return this.offlineMessagesRetrieved;
+    }
+
     private class MyKeyManager implements X509KeyManager {
         @Override
         public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
@@ -3031,6 +3078,10 @@ public class XmppConnection implements Runnable {
             return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
         }
 
+        public boolean pepConfigNodeMax() {
+            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_CONFIG_NODE_MAX);
+        }
+
         public boolean pepOmemoWhitelisted() {
             return hasDiscoFeature(
                     account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
@@ -3131,5 +3182,15 @@ public class XmppConnection implements Runnable {
         public boolean externalServiceDiscovery() {
             return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
         }
+
+        public boolean mds() {
+            return pepPublishOptions()
+                    && pepConfigNodeMax()
+                    && Config.MESSAGE_DISPLAYED_SYNCHRONIZATION;
+        }
+
+        public boolean mdsServerAssist() {
+            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED);
+        }
     }
 }

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

@@ -219,8 +219,7 @@ public abstract class AbstractJingleConnection {
         if (isTerminated()) {
             this.jingleConnectionManager.finishConnectionOrThrow(this);
         } else {
-            throw new AssertionError(
-                    String.format("Unable to call finish from %s", this.state));
+            throw new AssertionError(String.format("Unable to call finish from %s", this.state));
         }
     }
 
@@ -348,7 +347,7 @@ public abstract class AbstractJingleConnection {
         return features != null && features.contains(feature);
     }
 
-    public static class Id implements OngoingRtpSession {
+    public static class Id {
         public final Account account;
         public final Jid with;
         public final String sessionId;
@@ -400,17 +399,14 @@ public abstract class AbstractJingleConnection {
             return Objects.hashCode(account.getUuid(), with, sessionId);
         }
 
-        @Override
         public Account getAccount() {
             return account;
         }
 
-        @Override
         public Jid getWith() {
             return with;
         }
 
-        @Override
         public String getSessionId() {
             return sessionId;
         }

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

@@ -1,8 +1,11 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import android.telecom.VideoProfile;
 import android.util.Base64;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.google.common.base.Objects;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
@@ -21,15 +24,15 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.RtpSessionStatus;
 import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.CallIntegration;
+import eu.siacs.conversations.services.CallIntegrationConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
-import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
-import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
@@ -53,9 +56,8 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 public class JingleConnectionManager extends AbstractConnectionManager {
-    static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
+    public static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
             Executors.newSingleThreadScheduledExecutor();
-    final ToneManager toneManager;
     private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals =
             new HashMap<>();
     private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection>
@@ -66,7 +68,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
     public JingleConnectionManager(XmppConnectionService service) {
         super(service);
-        this.toneManager = new ToneManager(service);
     }
 
     static String nextRandomId() {
@@ -77,15 +78,20 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
     public void deliverPacket(final Account account, final JinglePacket packet) {
         final String sessionId = packet.getSessionId();
+        final JinglePacket.Action action = packet.getAction();
         if (sessionId == null) {
             respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
             return;
         }
+        if (action == null) {
+            respondWithJingleError(account, packet, null, "bad-request", "cancel");
+            return;
+        }
         final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet);
         final AbstractJingleConnection existingJingleConnection = connections.get(id);
         if (existingJingleConnection != null) {
             existingJingleConnection.deliverPacket(packet);
-        } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) {
+        } else if (action == JinglePacket.Action.SESSION_INITIATE) {
             final Jid from = packet.getFrom();
             final Content content = packet.getJingleContent();
             final String descriptionNamespace =
@@ -99,7 +105,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                         this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id));
                 final boolean stranger =
                         isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
-                final boolean busy = isBusy() != null;
+                final boolean busy = isBusy();
                 if (busy || sessionEnded || stranger) {
                     Log.d(
                             Config.LOGTAG,
@@ -110,13 +116,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                                     + sessionEnded
                                     + ", stranger="
                                     + stranger);
-                    mXmppConnectionService.sendIqPacket(
-                            account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
-                    final JinglePacket sessionTermination =
-                            new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
-                    sessionTermination.setTo(id.with);
-                    sessionTermination.setReason(Reason.BUSY, null);
-                    mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
+                    sendSessionTerminate(account, packet, id);
                     if (busy || stranger) {
                         writeLogMissedIncoming(
                                 account,
@@ -137,33 +137,58 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             connections.put(id, connection);
             mXmppConnectionService.updateConversationUi();
             connection.deliverPacket(packet);
+            if (connection instanceof JingleRtpConnection rtpConnection) {
+                addNewIncomingCall(rtpConnection);
+            }
         } else {
             Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
             respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
         }
     }
 
+    private void addNewIncomingCall(final JingleRtpConnection rtpConnection) {
+        if (rtpConnection.isTerminated()) {
+            Log.d(
+                    Config.LOGTAG,
+                    "skip call integration because something must have gone during initiate");
+            return;
+        }
+        if (CallIntegrationConnectionService.addNewIncomingCall(
+                mXmppConnectionService, rtpConnection.getId())) {
+            return;
+        }
+        rtpConnection.integrationFailure();
+    }
+
+    private void sendSessionTerminate(
+            final Account account, final IqPacket request, final AbstractJingleConnection.Id id) {
+        mXmppConnectionService.sendIqPacket(
+                account, request.generateResponse(IqPacket.TYPE.RESULT), null);
+        final JinglePacket sessionTermination =
+                new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
+        sessionTermination.setTo(id.with);
+        sessionTermination.setReason(Reason.BUSY, null);
+        mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
+    }
+
     private boolean isUsingClearNet(final Account account) {
         return !account.isOnion() && !mXmppConnectionService.useTorToConnect();
     }
 
-    public String isBusy() {
-        if (mXmppConnectionService.isPhoneInCall()) {
-            return "isPhoneInCall";
-        }
-        for (AbstractJingleConnection connection : this.connections.values()) {
-            if (connection instanceof JingleRtpConnection) {
-                if (((JingleRtpConnection) connection).isTerminated()) {
+    public boolean isBusy() {
+        for (final AbstractJingleConnection connection : this.connections.values()) {
+            if (connection instanceof JingleRtpConnection rtpConnection) {
+                if (connection.isTerminated() && rtpConnection.getCallIntegration().isDestroyed()) {
                     continue;
                 }
-                return "connection !isTerminated";
+                return true;
             }
         }
         synchronized (this.rtpSessionProposals) {
-            if (this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED)) return "discovered";
-            if (this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING)) return "searching";
-            if (this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED)) return "searching_acknolwedged";
-            return null;
+            if (this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED)) return true;
+            if (this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING)) return true;
+            if (this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED)) return true;
+            return false;
         }
     }
 
@@ -181,17 +206,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         return false;
     }
 
-    public void notifyPhoneCallStarted() {
-        for (AbstractJingleConnection connection : connections.values()) {
-            if (connection instanceof JingleRtpConnection rtpConnection) {
-                if (rtpConnection.isTerminated()) {
-                    continue;
-                }
-                rtpConnection.notifyPhoneCall();
-            }
-        }
-    }
-
     private Optional<RtpSessionProposal> findMatchingSessionProposal(
             final Account account, final Jid with, final Set<Media> media) {
         synchronized (this.rtpSessionProposals) {
@@ -248,9 +262,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     void respondWithJingleError(
             final Account account,
             final IqPacket original,
-            String jingleCondition,
-            String condition,
-            String conditionType) {
+            final String jingleCondition,
+            final String condition,
+            final String conditionType) {
         final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
         final Element error = response.addChild("error");
         error.setAttribute("type", conditionType);
@@ -389,6 +403,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                         this.connections.put(id, rtpConnection);
                         rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
                         rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
+                        addNewIncomingCall(rtpConnection);
                         // TODO actually do the automatic accept?!
                     } else {
                         Log.d(
@@ -402,7 +417,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 }
                 final boolean stranger =
                         isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
-                if (isBusy() != null || stranger) {
+                if (isBusy() || stranger) {
                     writeLogMissedIncoming(
                             account,
                             id.with.asBareJid(),
@@ -438,6 +453,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     this.connections.put(id, rtpConnection);
                     rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
                     rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
+                    addNewIncomingCall(rtpConnection);
                 }
             } else {
                 Log.d(
@@ -456,7 +472,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 if (proposal != null) {
                     rtpSessionProposals.remove(proposal);
                     final JingleRtpConnection rtpConnection =
-                            new JingleRtpConnection(this, id, account.getJid());
+                            new JingleRtpConnection(
+                                    this, id, account.getJid(), proposal.callIntegration);
                     rtpConnection.setProposedMedia(proposal.media);
                     this.connections.put(id, rtpConnection);
                     rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
@@ -488,10 +505,12 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             final RtpSessionProposal proposal =
                     getRtpSessionProposal(account, from.asBareJid(), sessionId);
             synchronized (rtpSessionProposals) {
-                if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
+                if (proposal != null) {
+                    setTerminalSessionState(proposal, RtpEndUserState.DECLINED_OR_BUSY);
+                    rtpSessionProposals.remove(proposal);
+                    proposal.callIntegration.busy();
                     writeLogMissedOutgoing(
                             account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
-                    toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
                     mXmppConnectionService.notifyJingleRtpConnectionUpdate(
                             account,
                             proposal.with,
@@ -514,8 +533,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             Log.d(
                     Config.LOGTAG,
                     account.getJid()
-                            + ": retrieved out of order jingle message from "
+                            + ": received out of order jingle message from="
                             + from
+                            + ", message="
                             + message
                             + ", addressedDirectly="
                             + addressedDirectly);
@@ -602,11 +622,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
         for (final Map.Entry<AbstractJingleConnection.Id, AbstractJingleConnection> entry :
                 this.connections.entrySet()) {
-            if (entry.getValue() instanceof JingleRtpConnection) {
+            if (entry.getValue() instanceof JingleRtpConnection jingleRtpConnection) {
                 final AbstractJingleConnection.Id id = entry.getKey();
                 if (id.account == contact.getAccount()
                         && id.with.asBareJid().equals(contact.getJid().asBareJid())) {
-                    return Optional.of(id);
+                    return Optional.of(jingleRtpConnection);
                 }
             }
         }
@@ -627,8 +647,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         return Optional.absent();
     }
 
-    void finishConnection(final AbstractJingleConnection connection) {
-        this.connections.remove(connection.getId());
+    public JingleRtpConnection getOngoingRtpConnection() {
+        for(final AbstractJingleConnection jingleConnection : this.connections.values()) {
+            if (jingleConnection instanceof JingleRtpConnection jingleRtpConnection) {
+                if (jingleRtpConnection.isTerminated()) {
+                    continue;
+                }
+                return jingleRtpConnection;
+            }
+        }
+        return null;
     }
 
     void finishConnectionOrThrow(final AbstractJingleConnection connection) {
@@ -642,17 +670,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     public boolean fireJingleRtpConnectionStateUpdates() {
-        boolean firedUpdates = false;
         for (final AbstractJingleConnection connection : this.connections.values()) {
             if (connection instanceof JingleRtpConnection jingleRtpConnection) {
                 if (jingleRtpConnection.isTerminated()) {
                     continue;
                 }
                 jingleRtpConnection.fireStateUpdate();
-                firedUpdates = true;
+                return true;
             }
         }
-        return firedUpdates;
+        return false;
     }
 
     public void retractSessionProposal(final Account account, final Jid with) {
@@ -665,20 +692,32 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 }
             }
             if (matchingProposal != null) {
-                retractSessionProposal(matchingProposal);
+                retractSessionProposal(matchingProposal, false);
             }
         }
     }
 
-    private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) {
+    private void retractSessionProposal(final RtpSessionProposal rtpSessionProposal) {
+        retractSessionProposal(rtpSessionProposal, true);
+    }
+
+    private void retractSessionProposal(
+            final RtpSessionProposal rtpSessionProposal, final boolean refresh) {
         final Account account = rtpSessionProposal.account;
-        toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media);
         Log.d(
                 Config.LOGTAG,
                 account.getJid().asBareJid()
                         + ": retracting rtp session proposal with "
                         + rtpSessionProposal.with);
         this.rtpSessionProposals.remove(rtpSessionProposal);
+        rtpSessionProposal.callIntegration.retracted();
+        if (refresh) {
+            mXmppConnectionService.notifyJingleRtpConnectionUpdate(
+                    account,
+                    rtpSessionProposal.with,
+                    rtpSessionProposal.sessionId,
+                    RtpEndUserState.RETRACTED);
+        }
         final MessagePacket messagePacket =
                 mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
         writeLogMissedOutgoing(
@@ -690,65 +729,104 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         mXmppConnectionService.sendMessagePacket(account, messagePacket);
     }
 
-    public String initializeRtpSession(
+    public JingleRtpConnection initializeRtpSession(
             final Account account, final Jid with, final Set<Media> media) {
         final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
         final JingleRtpConnection rtpConnection =
                 new JingleRtpConnection(this, id, account.getJid());
         rtpConnection.setProposedMedia(media);
+        rtpConnection.getCallIntegration().startAudioRouting();
         this.connections.put(id, rtpConnection);
         rtpConnection.sendSessionInitiate();
-        return id.sessionId;
+        return rtpConnection;
     }
 
-    public String proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
+    public @Nullable RtpSessionProposal proposeJingleRtpSession(
+            final Account account, final Jid with, final Set<Media> media) {
         synchronized (this.rtpSessionProposals) {
-            for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
+            for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
                     this.rtpSessionProposals.entrySet()) {
-                RtpSessionProposal proposal = entry.getKey();
+                final RtpSessionProposal proposal = entry.getKey();
                 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
                     final DeviceDiscoveryState preexistingState = entry.getValue();
                     if (preexistingState != null
                             && preexistingState != DeviceDiscoveryState.FAILED) {
                         final RtpEndUserState endUserState = preexistingState.toEndUserState();
-                        toneManager.transition(endUserState, media);
                         mXmppConnectionService.notifyJingleRtpConnectionUpdate(
-                                account,
-                                with,
-                                proposal.sessionId,
-                                endUserState
-                        );
-                        return proposal.sessionId;
+                                account, with, proposal.sessionId, endUserState);
+                        return proposal;
                     }
                 }
             }
-            String busyCode = isBusy();
-            if (busyCode != null) {
-                String sessionId = hasMatchingRtpSession(account, with, media);
-                if (sessionId != null) {
-                    Log.d(Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us: " + sessionId);
-                    return sessionId;
+            if (isBusy()) {
+                if (hasMatchingRtpSession(account, with, media) != null) {
+                    Log.d(
+                            Config.LOGTAG,
+                            "ignoring request to propose jingle session because the other party already created one for us");
+                    // TODO return something that we can parse the connection of of
+                    return null;
                 }
-                throw new IllegalStateException("There is already a running RTP session: " + busyCode);
+                throw new IllegalStateException("There is already a running RTP session");
             }
+            final CallIntegration callIntegration =
+                    new CallIntegration(mXmppConnectionService.getApplicationContext());
+            callIntegration.setVideoState(
+                    Media.audioOnly(media)
+                            ? VideoProfile.STATE_AUDIO_ONLY
+                            : VideoProfile.STATE_BIDIRECTIONAL);
+            callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
+            callIntegration.startAudioRouting();
             final RtpSessionProposal proposal =
-                    RtpSessionProposal.of(account, with.asBareJid(), media);
+                    RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration);
+            callIntegration.setCallback(new ProposalStateCallback(proposal));
             this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
             mXmppConnectionService.notifyJingleRtpConnectionUpdate(
                     account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
             final MessagePacket messagePacket =
                     mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
             mXmppConnectionService.sendMessagePacket(account, messagePacket);
-            return proposal.sessionId;
+            return proposal;
         }
     }
 
+    public void sendJingleMessageFinish(
+            final Contact contact, final String sessionId, final Reason reason) {
+        final var account = contact.getAccount();
+        final MessagePacket messagePacket =
+                mXmppConnectionService
+                        .getMessageGenerator()
+                        .sessionFinish(contact.getJid(), sessionId, reason);
+        mXmppConnectionService.sendMessagePacket(account, messagePacket);
+    }
+
+    public Optional<RtpSessionProposal> matchingProposal(final Account account, final Jid with) {
+        synchronized (this.rtpSessionProposals) {
+            for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
+                    this.rtpSessionProposals.entrySet()) {
+                final RtpSessionProposal proposal = entry.getKey();
+                if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
+                    return Optional.of(proposal);
+                }
+            }
+        }
+        return Optional.absent();
+    }
+
     public boolean hasMatchingProposal(final Account account, final Jid with) {
         synchronized (this.rtpSessionProposals) {
-            for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
+            for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
                     this.rtpSessionProposals.entrySet()) {
+                final var state = entry.getValue();
                 final RtpSessionProposal proposal = entry.getKey();
                 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
+                    // CallIntegrationConnectionService starts RtpSessionActivity with ACTION_VIEW
+                    // and an EXTRA_LAST_REPORTED_STATE of DISCOVERING devices. however due to
+                    // possible race conditions the state might have already moved on so we are
+                    // going
+                    // to update the UI
+                    final RtpEndUserState endUserState = state.toEndUserState();
+                    mXmppConnectionService.notifyJingleRtpConnectionUpdate(
+                            account, proposal.with, proposal.sessionId, endUserState);
                     return true;
                 }
             }
@@ -778,7 +856,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             sid = null;
         }
         if (sid == null) {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid");
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid() + ": unable to deliver ibb packet. missing sid");
             account.getXmppConnection()
                     .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
             return;
@@ -802,7 +882,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 }
             }
         }
-        Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet with sid="+sid);
+        Log.d(
+                Config.LOGTAG,
+                account.getJid().asBareJid() + ": unable to deliver ibb packet with sid=" + sid);
         account.getXmppConnection()
                 .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
     }
@@ -828,6 +910,21 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         return null;
     }
 
+    public JingleRtpConnection findJingleRtpConnection(final Account account, final Jid with) {
+        for (final AbstractJingleConnection connection : this.connections.values()) {
+            if (connection instanceof JingleRtpConnection rtpConnection) {
+                if (rtpConnection.isTerminated()) {
+                    continue;
+                }
+                final var id = rtpConnection.getId();
+                if (id.account == account && account.getJid().equals(with)) {
+                    return rtpConnection;
+                }
+            }
+        }
+        return null;
+    }
+
     private void resendSessionProposals(final Account account) {
         synchronized (this.rtpSessionProposals) {
             for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
@@ -856,7 +953,12 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             final DeviceDiscoveryState currentState =
                     sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal);
             if (currentState == null) {
-                Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId);
+                Log.d(
+                        Config.LOGTAG,
+                        "unable to find session proposal for session id "
+                                + sessionId
+                                + " target="
+                                + target);
                 return;
             }
             if (currentState == DeviceDiscoveryState.DISCOVERED) {
@@ -865,11 +967,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                         "session proposal already at discovered. not going to fall back");
                 return;
             }
-            this.rtpSessionProposals.put(sessionProposal, target);
-            final RtpEndUserState endUserState = target.toEndUserState();
-            toneManager.transition(endUserState, sessionProposal.media);
-            mXmppConnectionService.notifyJingleRtpConnectionUpdate(
-                    account, sessionProposal.with, sessionProposal.sessionId, endUserState);
+
             Log.d(
                     Config.LOGTAG,
                     account.getJid().asBareJid()
@@ -877,6 +975,30 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                             + sessionId
                             + " as "
                             + target);
+
+            final RtpEndUserState endUserState = target.toEndUserState();
+
+            if (target == DeviceDiscoveryState.FAILED) {
+                Log.d(Config.LOGTAG, "removing session proposal after failure");
+                setTerminalSessionState(sessionProposal, endUserState);
+                this.rtpSessionProposals.remove(sessionProposal);
+                sessionProposal.getCallIntegration().error();
+                mXmppConnectionService.notifyJingleRtpConnectionUpdate(
+                        account,
+                        sessionProposal.with,
+                        sessionProposal.sessionId,
+                        endUserState);
+                return;
+            }
+
+            this.rtpSessionProposals.put(sessionProposal, target);
+
+            if (endUserState == RtpEndUserState.RINGING) {
+                sessionProposal.callIntegration.setDialing();
+            }
+
+            mXmppConnectionService.notifyJingleRtpConnectionUpdate(
+                    account, sessionProposal.with, sessionProposal.sessionId, endUserState);
         }
     }
 
@@ -935,6 +1057,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 PersistableSessionId.of(id), new TerminatedRtpSession(state, media));
     }
 
+    void setTerminalSessionState(final RtpSessionProposal proposal, final RtpEndUserState state) {
+        this.terminatedSessions.put(
+                PersistableSessionId.of(proposal), new TerminatedRtpSession(state, proposal.media));
+    }
+
     public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) {
         return this.terminatedSessions.getIfPresent(new PersistableSessionId(with, sessionId));
     }
@@ -948,10 +1075,14 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             this.sessionId = sessionId;
         }
 
-        public static PersistableSessionId of(AbstractJingleConnection.Id id) {
+        public static PersistableSessionId of(final AbstractJingleConnection.Id id) {
             return new PersistableSessionId(id.with, id.sessionId);
         }
 
+        public static PersistableSessionId of(final RtpSessionProposal proposal) {
+            return new PersistableSessionId(proposal.with, proposal.sessionId);
+        }
+
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
@@ -996,16 +1127,27 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         public final String sessionId;
         public final Set<Media> media;
         private final Account account;
+        private final CallIntegration callIntegration;
 
-        private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) {
+        private RtpSessionProposal(
+                Account account,
+                Jid with,
+                String sessionId,
+                Set<Media> media,
+                final CallIntegration callIntegration) {
             this.account = account;
             this.with = with;
             this.sessionId = sessionId;
             this.media = media;
+            this.callIntegration = callIntegration;
         }
 
-        public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) {
-            return new RtpSessionProposal(account, with, nextRandomId(), media);
+        public static RtpSessionProposal of(
+                Account account,
+                Jid with,
+                Set<Media> media,
+                final CallIntegration callIntegration) {
+            return new RtpSessionProposal(account, with, nextRandomId(), media, callIntegration);
         }
 
         @Override
@@ -1037,5 +1179,50 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         public String getSessionId() {
             return sessionId;
         }
+
+        @Override
+        public CallIntegration getCallIntegration() {
+            return this.callIntegration;
+        }
+
+        @Override
+        public Set<Media> getMedia() {
+            return this.media;
+        }
+    }
+
+    public class ProposalStateCallback implements CallIntegration.Callback {
+
+        private final RtpSessionProposal proposal;
+
+        public ProposalStateCallback(final RtpSessionProposal proposal) {
+            this.proposal = proposal;
+        }
+
+        @Override
+        public void onCallIntegrationShowIncomingCallUi() {}
+
+        @Override
+        public void onCallIntegrationDisconnect() {
+            Log.d(Config.LOGTAG, "a phone call has just been started. retracting proposal");
+            retractSessionProposal(this.proposal);
+        }
+
+        @Override
+        public void onAudioDeviceChanged(
+                final CallIntegration.AudioDevice selectedAudioDevice,
+                final Set<CallIntegration.AudioDevice> availableAudioDevices) {
+            mXmppConnectionService.notifyJingleRtpConnectionUpdate(
+                    selectedAudioDevice, availableAudioDevices);
+        }
+
+        @Override
+        public void onCallIntegrationReject() {}
+
+        @Override
+        public void onCallIntegrationAnswer() {}
+
+        @Override
+        public void onCallIntegrationSilence() {}
     }
 }

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

@@ -253,6 +253,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                     id.account.getJid().asBareJid() + ": improperly formatted contents",
                     Throwables.getRootCause(e));
             respondOk(jinglePacket);
+            terminateTransport();
             sendSessionTerminate(Reason.of(e), e.getMessage());
             return;
         }
@@ -534,6 +535,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         if (isTerminated()) {
             return;
         }
+        terminateTransport();
         final Throwable rootCause = Throwables.getRootCause(throwable);
         Log.d(Config.LOGTAG, "unable to send session accept", rootCause);
         sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
@@ -603,6 +605,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                     id.account.getJid().asBareJid() + ": improperly formatted contents",
                     Throwables.getRootCause(e));
             respondOk(jinglePacket);
+            terminateTransport();
             sendSessionTerminate(Reason.of(e), e.getMessage());
             return;
         }
@@ -646,6 +649,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                     id.account.getJid().asBareJid() + ": improperly formatted contents",
                     Throwables.getRootCause(e));
             respondOk(jinglePacket);
+            terminateTransport();
             sendSessionTerminate(Reason.of(e), e.getMessage());
             return;
         }
@@ -708,6 +712,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         } else if (transportInfo
                 instanceof SocksByteStreamsTransportInfo.CandidateUsed candidateUsed) {
             if (!socksBytestreamsTransport.setCandidateUsed(candidateUsed.cid)) {
+                terminateTransport();
                 sendSessionTerminate(
                         Reason.FAILED_TRANSPORT,
                         String.format(
@@ -734,6 +739,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                     id.account.getJid().asBareJid() + ": improperly formatted contents",
                     Throwables.getRootCause(e));
             respondOk(jinglePacket);
+            terminateTransport();
             sendSessionTerminate(Reason.of(e), e.getMessage());
             return;
         }
@@ -843,6 +849,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         if (transport == null) {
             return;
         }
+        // TODO consider setting transport callback to null. requires transport to handle null
+        // callback
+        // transport.setTransportCallback(null);
         transport.terminate();
         this.transport = null;
     }
@@ -873,8 +882,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                     }
 
                     @Override
-                    public void onFailure(@NonNull Throwable throwable) {
-                        onFileTransmissionFailed(throwable);
+                    public void onFailure(@NonNull final Throwable throwable) {
+                        // The state transition in here should be synchronized to not race with the
+                        // state transition in receiveSessionTerminate
+                        synchronized (JingleFileTransferConnection.this) {
+                            onFileTransmissionFailed(throwable);
+                        }
                     }
                 },
                 MoreExecutors.directExecutor());
@@ -980,7 +993,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
     public void onTransportSetupFailed() {
         final var transport = this.transport;
         if (transport == null) {
-            // this really is not supposed to happen
+            // this can happen on IQ timeouts
+            if (isTerminated()) {
+                return;
+            }
             sendSessionTerminate(Reason.FAILED_APPLICATION, null);
             return;
         }
@@ -1227,6 +1243,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         if (transition(target)) {
             // we change state before terminating transport so we don't consume the following
             // IOException and turn it into a connectivity error
+
+            if (isInitiator() && reason == Reason.CANCEL) {
+                // message hooks have already run so we need to mark to persist the 'cancelled'
+                // status
+                xmppConnectionService.markMessage(
+                        message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED);
+            }
             terminateTransport();
             final JinglePacket jinglePacket =
                     new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);

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

@@ -1,5 +1,8 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import android.content.Intent;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
 import android.util.Log;
 import android.os.Environment;
 
@@ -13,13 +16,11 @@ import com.google.common.base.Stopwatch;
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -30,12 +31,13 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
+import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.RtpSessionStatus;
-import eu.siacs.conversations.services.AppRTCAudioManager;
-import eu.siacs.conversations.utils.IP;
+import eu.siacs.conversations.services.CallIntegration;
+import eu.siacs.conversations.ui.RtpSessionActivity;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
@@ -71,17 +73,22 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 public class JingleRtpConnection extends AbstractJingleConnection
-        implements WebRTCWrapper.EventCallback {
+        implements WebRTCWrapper.EventCallback, CallIntegration.Callback, OngoingRtpSession {
 
     public static final List<State> STATES_SHOWING_ONGOING_CALL =
             Arrays.asList(
-                    State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
+                    State.PROPOSED,
+                    State.PROCEED,
+                    State.SESSION_INITIALIZED_PRE_APPROVED,
+                    State.SESSION_ACCEPTED);
     private static final long BUSY_TIME_OUT = 30;
 
     private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
-    private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>>
+    private final Queue<
+                    Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>>
             pendingIceCandidates = new LinkedList<>();
     private final OmemoVerification omemoVerification = new OmemoVerification();
+    public final CallIntegration callIntegration;
     private final Message message;
 
     private Set<Media> proposedMedia;
@@ -95,7 +102,28 @@ public class JingleRtpConnection extends AbstractJingleConnection
     private ScheduledFuture<?> ringingTimeoutFuture;
     private final long created = System.currentTimeMillis() / 1000L;
 
-    JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
+    JingleRtpConnection(
+            final JingleConnectionManager jingleConnectionManager,
+            final Id id,
+            final Jid initiator) {
+        this(
+                jingleConnectionManager,
+                id,
+                initiator,
+                new CallIntegration(
+                        jingleConnectionManager
+                                .getXmppConnectionService()
+                                .getApplicationContext()));
+        this.callIntegration.setAddress(
+                CallIntegration.address(id.with.asBareJid()), TelecomManager.PRESENTATION_ALLOWED);
+        this.callIntegration.setInitialized();
+    }
+
+    JingleRtpConnection(
+            final JingleConnectionManager jingleConnectionManager,
+            final Id id,
+            final Jid initiator,
+            final CallIntegration callIntegration) {
         super(jingleConnectionManager, id, initiator);
         final Conversation conversation =
                 jingleConnectionManager
@@ -107,6 +135,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
                         isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED,
                         Message.TYPE_RTP_SESSION,
                         id.sessionId);
+        this.callIntegration = callIntegration;
+        this.callIntegration.setCallback(this);
     }
 
     @Override
@@ -231,8 +261,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private void receiveTransportInfo(
             final JinglePacket jinglePacket, final RtpContentMap contentMap) {
-        final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> candidates =
-                contentMap.contents.entrySet();
+        final Set<Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>>
+                candidates = contentMap.contents.entrySet();
         final RtpContentMap remote = getRemoteContentMap();
         final Set<String> remoteContentIds =
                 remote == null ? Collections.emptySet() : remote.contents.keySet();
@@ -1004,14 +1034,17 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void processCandidates(
-            final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> contents) {
-        for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : contents) {
+            final Set<Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>>
+                    contents) {
+        for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+                content : contents) {
             processCandidate(content);
         }
     }
 
     private void processCandidate(
-            final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content) {
+            final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
+                    content) {
         final RtpContentMap rtpContentMap = getRemoteContentMap();
         final List<String> indices = toIdentificationTags(rtpContentMap);
         final String sdpMid = content.getKey(); // aka content name
@@ -1167,6 +1200,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             target = State.SESSION_INITIALIZED_PRE_APPROVED;
         } else {
             target = State.SESSION_INITIALIZED;
+            setProposedMedia(contentMap.getMedia());
         }
         if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
             respondOk(jinglePacket);
@@ -1381,7 +1415,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void addIceCandidatesFromBlackLog() {
-        Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> foo;
+        Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> foo;
         while ((foo = this.pendingIceCandidates.poll()) != null) {
             processCandidate(foo);
             Log.d(
@@ -1637,7 +1671,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
                                     + from
                                     + " for "
                                     + media);
-                    this.proposedMedia = Sets.newHashSet(media);
+                    this.setProposedMedia(Sets.newHashSet(media));
                 })) {
             if (serverMsgId != null) {
                 this.message.setServerMsgId(serverMsgId);
@@ -1657,6 +1691,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void startRinging() {
+        this.callIntegration.setRinging();
         Log.d(
                 Config.LOGTAG,
                 id.account.getJid().asBareJid()
@@ -1666,6 +1701,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
         ringingTimeoutFuture =
                 jingleConnectionManager.schedule(
                         this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
+        if (CallIntegration.selfManaged(xmppConnectionService)) {
+            return;
+        }
         xmppConnectionService.getNotificationService().startRinging(id, getMedia());
     }
 
@@ -1955,12 +1993,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
         sendSessionTerminate(reason, null);
     }
 
-
     protected void sendSessionTerminate(final Reason reason, final String text) {
-        sendSessionTerminate(reason,text, this::writeLogMessage);
+        sendSessionTerminate(reason, text, this::writeLogMessage);
+        sendJingleMessageFinish(reason);
     }
 
-
     private void sendTransportInfo(
             final String contentName, IceUdpTransportInfo.Candidate candidate) {
         final RtpContentMap transportInfo;
@@ -2063,6 +2100,56 @@ public class JingleRtpConnection extends AbstractJingleConnection
         };
     }
 
+    private boolean isPeerConnectionConnected() {
+        try {
+            return webRTCWrapper.getState() == PeerConnection.PeerConnectionState.CONNECTED;
+        } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
+            return false;
+        }
+    }
+
+    private void updateCallIntegrationState() {
+        switch (this.state) {
+            case NULL, PROPOSED, SESSION_INITIALIZED -> {
+                if (isInitiator()) {
+                    this.callIntegration.setDialing();
+                } else {
+                    this.callIntegration.setRinging();
+                }
+            }
+            case PROCEED, SESSION_INITIALIZED_PRE_APPROVED -> {
+                if (isInitiator()) {
+                    this.callIntegration.setDialing();
+                } else {
+                    this.callIntegration.setInitialized();
+                }
+            }
+            case SESSION_ACCEPTED -> {
+                if (isPeerConnectionConnected()) {
+                    this.callIntegration.setActive();
+                } else {
+                    this.callIntegration.setInitialized();
+                }
+            }
+            case REJECTED, REJECTED_RACED, TERMINATED_DECLINED_OR_BUSY -> {
+                if (isInitiator()) {
+                    this.callIntegration.busy();
+                } else {
+                    this.callIntegration.rejected();
+                }
+            }
+            case TERMINATED_SUCCESS -> this.callIntegration.success();
+            case ACCEPTED -> this.callIntegration.accepted();
+            case RETRACTED, RETRACTED_RACED, TERMINATED_CANCEL_OR_TIMEOUT -> this.callIntegration
+                    .retracted();
+            case TERMINATED_CONNECTIVITY_ERROR,
+                    TERMINATED_APPLICATION_FAILURE,
+                    TERMINATED_SECURITY_ERROR -> this.callIntegration.error();
+            default -> throw new IllegalStateException(
+                    String.format("%s is not handled", this.state));
+        }
+    }
+
     public ContentAddition getPendingContentAddition() {
         final RtpContentMap in = this.incomingContentAdd;
         final RtpContentMap out = this.outgoingContentAdd;
@@ -2144,15 +2231,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
     }
 
-    public void notifyPhoneCall() {
-        Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
-        if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
-            rejectCall();
-        } else {
-            endCall();
-        }
-    }
-
     public synchronized void rejectCall() {
         if (isTerminated()) {
             Log.w(
@@ -2168,6 +2246,24 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
     }
 
+    public synchronized void integrationFailure() {
+        final var state = getState();
+        if (state == State.PROPOSED) {
+            Log.e(
+                    Config.LOGTAG,
+                    id.account.getJid().asBareJid()
+                            + ": failed call integration in state proposed");
+            rejectCallFromProposed();
+        } else if (state == State.SESSION_INITIALIZED) {
+            Log.e(Config.LOGTAG, id.account.getJid().asBareJid() + ": failed call integration");
+            this.webRTCWrapper.close();
+            sendSessionTerminate(Reason.FAILED_APPLICATION, "CallIntegration failed");
+        } else {
+            throw new IllegalStateException(
+                    String.format("Can not fail integration in state %s", state));
+        }
+    }
+
     public synchronized void endCall() {
         if (isTerminated()) {
             Log.w(
@@ -2235,14 +2331,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
             final boolean trickle)
             throws WebRTCWrapper.InitializationException {
         this.jingleConnectionManager.ensureConnectionIsRegistered(this);
-        this.webRTCWrapper.setup(
-                this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media));
+        this.webRTCWrapper.setup(this.xmppConnectionService);
         this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle);
     }
 
     private void acceptCallFromProposed() {
         transitionOrThrow(State.PROCEED);
         xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
+        this.callIntegration.startAudioRouting();
         this.sendJingleMessage("accept", id.account.getJid().asBareJid());
         this.sendJingleMessage("proceed");
     }
@@ -2291,6 +2387,15 @@ public class JingleRtpConnection extends AbstractJingleConnection
         xmppConnectionService.sendMessagePacket(id.account, messagePacket);
     }
 
+    private void sendJingleMessageFinish(final Reason reason) {
+        final var account = id.getAccount();
+        final MessagePacket messagePacket =
+                xmppConnectionService
+                        .getMessageGenerator()
+                        .sessionFinish(id.with, id.sessionId, reason);
+        xmppConnectionService.sendMessagePacket(account, messagePacket);
+    }
+
     private boolean isOmemoEnabled() {
         final Conversational conversational = message.getConversation();
         if (conversational instanceof Conversation) {
@@ -2302,10 +2407,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private void acceptCallFromSessionInitialized() {
         xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
+        this.callIntegration.startAudioRouting();
         sendSessionAccept();
     }
 
-
     @Override
     protected synchronized boolean transition(final State target, final Runnable runnable) {
         if (super.transition(target, runnable)) {
@@ -2546,8 +2651,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
     private void modifyLocalContentMap(final RtpContentMap rtpContentMap) {
         final RtpContentMap activeContents = rtpContentMap.activeContents();
         setLocalContentMap(activeContents);
-        this.webRTCWrapper.switchSpeakerPhonePreference(
-                AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia()));
+        this.callIntegration.setAudioDeviceWhenAvailable(
+                CallIntegration.initialAudioDevice(activeContents.getMedia()));
         updateEndUserState();
     }
 
@@ -2580,8 +2685,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
         return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
     }
 
-    public AppRTCAudioManager getAudioManager() {
-        return webRTCWrapper.getAudioManager();
+    @Override
+    public CallIntegration getCallIntegration() {
+        return this.callIntegration;
     }
 
     public boolean isMicrophoneEnabled() {
@@ -2612,17 +2718,73 @@ public class JingleRtpConnection extends AbstractJingleConnection
         return webRTCWrapper.switchCamera();
     }
 
+    @Override
+    public synchronized void onCallIntegrationShowIncomingCallUi() {
+        if (isTerminated()) {
+            // there might be race conditions with the call integration service invoking this
+            // callback when the rtp session has already ended.
+            Log.w(
+                    Config.LOGTAG,
+                    "CallIntegration requested incoming call UI but session was already terminated");
+            return;
+        }
+        // TODO apparently this can be called too early as well?
+        xmppConnectionService.getNotificationService().startRinging(id, getMedia());
+    }
+
+    @Override
+    public void onCallIntegrationDisconnect() {
+        Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
+        if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
+            rejectCall();
+        } else {
+            endCall();
+        }
+    }
+
+    @Override
+    public void onCallIntegrationReject() {
+        Log.d(Config.LOGTAG, "rejecting call from system notification / call integration");
+        try {
+            rejectCall();
+        } catch (final IllegalStateException e) {
+            Log.w(Config.LOGTAG, "race condition on rejecting call from notification", e);
+        }
+    }
+
+    @Override
+    public void onCallIntegrationAnswer() {
+        // we need to start the UI to a) show it and b) be able to ask for permissions
+        final Intent intent = new Intent(xmppConnectionService, RtpSessionActivity.class);
+        intent.setAction(RtpSessionActivity.ACTION_ACCEPT_CALL);
+        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().toEscapedString());
+        intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
+        Log.d(Config.LOGTAG, "start activity to accept call from call integration");
+        xmppConnectionService.startActivity(intent);
+    }
+
+    @Override
+    public void onCallIntegrationSilence() {
+        xmppConnectionService.getNotificationService().stopSoundAndVibration();
+    }
+
     @Override
     public void onAudioDeviceChanged(
-            AppRTCAudioManager.AudioDevice selectedAudioDevice,
-            Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
+            final CallIntegration.AudioDevice selectedAudioDevice,
+            final Set<CallIntegration.AudioDevice> availableAudioDevices) {
+        Log.d(
+                Config.LOGTAG,
+                "onAudioDeviceChanged(" + selectedAudioDevice + "," + availableAudioDevices + ")");
         xmppConnectionService.notifyJingleRtpConnectionUpdate(
                 selectedAudioDevice, availableAudioDevices);
     }
 
     private void updateEndUserState() {
         final RtpEndUserState endUserState = getEndUserState();
-        jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
+        this.updateCallIntegrationState();
         xmppConnectionService.notifyJingleRtpConnectionUpdate(
                 id.account, id.with, id.sessionId, endUserState);
     }
@@ -2653,7 +2815,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
                     request,
                     (account, response) -> {
                         final var iceServers = IceServers.parse(response);
-                        if (iceServers.size() == 0) {
+                        if (iceServers.isEmpty()) {
                             Log.w(
                                     Config.LOGTAG,
                                     id.account.getJid().asBareJid()
@@ -2669,7 +2831,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
             onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
         }
     }
-    
+
     @Override
     protected void terminateTransport() {
         this.webRTCWrapper.close();
@@ -2679,6 +2841,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     protected void finish() {
         if (isTerminated()) {
             this.cancelRingingTimeout();
+            this.callIntegration.verifyDisconnected();
             this.webRTCWrapper.verifyClosed();
             this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
             super.finish();
@@ -2738,6 +2901,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     void setProposedMedia(final Set<Media> media) {
         this.proposedMedia = media;
+        this.callIntegration.setVideoState(
+                Media.audioOnly(media)
+                        ? VideoProfile.STATE_AUDIO_ONLY
+                        : VideoProfile.STATE_BIDIRECTIONAL);
+        this.callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
     }
 
     public void fireStateUpdate() {
@@ -2762,6 +2930,21 @@ public class JingleRtpConnection extends AbstractJingleConnection
         return remoteHasFeature(Namespace.SDP_OFFER_ANSWER);
     }
 
+    @Override
+    public Account getAccount() {
+        return id.account;
+    }
+
+    @Override
+    public Jid getWith() {
+        return id.with;
+    }
+
+    @Override
+    public String getSessionId() {
+        return id.sessionId;
+    }
+
     private interface OnIceServersDiscovered {
         void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
     }

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

@@ -1,10 +1,19 @@
 package eu.siacs.conversations.xmpp.jingle;
 
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.CallIntegration;
 import eu.siacs.conversations.xmpp.Jid;
 
+import java.util.Set;
+
 public interface OngoingRtpSession {
     Account getAccount();
+
     Jid getWith();
+
     String getSessionId();
+
+    CallIntegration getCallIntegration();
+
+    Set<Media> getMedia();
 }

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

@@ -1,20 +1,23 @@
 package eu.siacs.conversations.xmpp.jingle;
 
 public enum RtpEndUserState {
-    INCOMING_CALL, //received a 'propose' message
-    CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet
-    CONNECTED, //session-accepted and webrtc peer connection is connected
-    RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed
-    INCOMING_CONTENT_ADD, //session-accepted with a pending, incoming content-add
-    FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
-    RINGING, //'propose' has been sent out and it has been 184 acked
-    ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
-    ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through
-    ENDED, //close UI
-    DECLINED_OR_BUSY, //other party declined; no retry button
-    CONNECTIVITY_ERROR, //network error; retry button
-    CONNECTIVITY_LOST_ERROR, //network error but for call duration > 0
-    RETRACTED, //user pressed home or power button during 'ringing' - shows retry button
-    APPLICATION_ERROR, //something rather bad happened; libwebrtc failed or we got in IQ-error
-    SECURITY_ERROR //problem with DTLS (missing) or verification
+    INCOMING_CALL, // received a 'propose' message
+    CONNECTING, // session-initiate or session-accepted but no webrtc peer connection yet
+    CONNECTED, // session-accepted and webrtc peer connection is connected
+    RECONNECTING, // session-accepted and webrtc peer connection was connected once but is currently
+                  // disconnected or failed
+    INCOMING_CONTENT_ADD, // session-accepted with a pending, incoming content-add
+    FINDING_DEVICE, // 'propose' has been sent out; no 184 ack yet
+    RINGING, // 'propose' has been sent out and it has been 184 acked
+    ACCEPTING_CALL, // 'proceed' message has been sent; but no session-initiate has been received
+    ENDING_CALL, // libwebrt says 'closed' but session-terminate has not gone through
+    ENDED, // close UI
+    DECLINED_OR_BUSY, // other party declined; no retry button
+    CONTACT_OFFLINE, // when `JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK` is true this shows up when
+                     // the contact is offline, generally similar to BUSY
+    CONNECTIVITY_ERROR, // network error; retry button
+    CONNECTIVITY_LOST_ERROR, // network error but for call duration > 0
+    RETRACTED, // user pressed home or power button during 'ringing' - shows retry button
+    APPLICATION_ERROR, // something rather bad happened; libwebrtc failed or we got in IQ-error
+    SECURITY_ERROR // problem with DTLS (missing) or verification
 }

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

@@ -65,8 +65,14 @@ class TrackWrapper<T extends MediaStreamTrack> {
     public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver(
             @Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
         final RtpSender rtpSender = trackWrapper.rtpSender;
+        final String rtpSenderId;
+        try {
+            rtpSenderId = rtpSender.id();
+        } catch (final IllegalStateException e) {
+            return null;
+        }
         for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) {
-            if (transceiver.getSender().id().equals(rtpSender.id())) {
+            if (transceiver.getSender().id().equals(rtpSenderId)) {
                 return transceiver;
             }
         }

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

@@ -57,6 +57,9 @@ class VideoSourceWrapper {
             final EglBase.Context eglBaseContext) {
         final SurfaceTextureHelper surfaceTextureHelper =
                 SurfaceTextureHelper.create("webrtc", eglBaseContext);
+        if (surfaceTextureHelper == null) {
+            throw new IllegalStateException("Could not create SurfaceTextureHelper");
+        }
         this.videoSource = peerConnectionFactory.createVideoSource(false);
         this.cameraVideoCapturer.initialize(
                 surfaceTextureHelper, context, this.videoSource.getCapturerObserver());

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

@@ -1,10 +1,9 @@
 package eu.siacs.conversations.xmpp.jingle;
 
 import android.content.Context;
+import android.media.AudioManager;
 import android.media.ToneGenerator;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
 import android.util.Log;
 
 import com.google.common.base.Optional;
@@ -17,7 +16,6 @@ import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
 
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.services.AppRTCAudioManager;
 import eu.siacs.conversations.services.XmppConnectionService;
 
 import org.webrtc.AudioSource;
@@ -56,7 +54,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-@SuppressWarnings("UnstableApiUsage")
 public class WebRTCWrapper {
 
     private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
@@ -66,6 +63,7 @@ public class WebRTCWrapper {
             Executors.newSingleThreadExecutor();
 
     private static final int TONE_DURATION = 500;
+    private static final int DEFAULT_TONE_VOLUME = 60;
     private static final Map<String,Integer> TONE_CODES;
     static {
         ImmutableMap.Builder<String,Integer> builder = new ImmutableMap.Builder<>();
@@ -107,16 +105,6 @@ public class WebRTCWrapper {
     private final EventCallback eventCallback;
     private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
     private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
-    private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents =
-            new AppRTCAudioManager.AudioManagerEvents() {
-                @Override
-                public void onAudioDeviceChanged(
-                        AppRTCAudioManager.AudioDevice selectedAudioDevice,
-                        Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
-                    eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
-                }
-            };
-    private final Handler mainHandler = new Handler(Looper.getMainLooper());
     private TrackWrapper<AudioTrack> localAudioTrack = null;
     private TrackWrapper<VideoTrack> localVideoTrack = null;
     private VideoTrack remoteVideoTrack = null;
@@ -238,8 +226,6 @@ public class WebRTCWrapper {
             };
     @Nullable private PeerConnectionFactory peerConnectionFactory = null;
     @Nullable private PeerConnection peerConnection = null;
-    private AppRTCAudioManager appRTCAudioManager = null;
-    private ToneManager toneManager = null;
     private Context context = null;
     private EglBase eglBase = null;
     private VideoSourceWrapper videoSourceWrapper;
@@ -256,10 +242,7 @@ public class WebRTCWrapper {
         }
     }
 
-    public void setup(
-            final XmppConnectionService service,
-            @Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
-            throws InitializationException {
+    public void setup(final XmppConnectionService service) throws InitializationException {
         try {
             PeerConnectionFactory.initialize(
                     PeerConnectionFactory.InitializationOptions.builder(service)
@@ -274,16 +257,6 @@ public class WebRTCWrapper {
             throw new InitializationException("Unable to create EGL base", e);
         }
         this.context = service;
-        this.toneManager = service.getJingleConnectionManager().toneManager;
-        mainHandler.post(
-                () -> {
-                    appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
-                    toneManager.setAppRtcAudioManagerHasControl(true);
-                    appRTCAudioManager.start(audioManagerEvents);
-                    eventCallback.onAudioDeviceChanged(
-                            appRTCAudioManager.getSelectedAudioDevice(),
-                            appRTCAudioManager.getAudioDevices());
-                });
     }
 
     synchronized void initializePeerConnection(
@@ -486,16 +459,11 @@ public class WebRTCWrapper {
         final PeerConnection peerConnection = this.peerConnection;
         final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
         final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
-        final AppRTCAudioManager audioManager = this.appRTCAudioManager;
         final EglBase eglBase = this.eglBase;
         if (peerConnection != null) {
             this.peerConnection = null;
             dispose(peerConnection);
         }
-        if (audioManager != null) {
-            toneManager.setAppRtcAudioManagerHasControl(false);
-            mainHandler.post(audioManager::stop);
-        }
         this.localVideoTrack = null;
         this.remoteVideoTrack = null;
         if (videoSourceWrapper != null) {
@@ -522,8 +490,8 @@ public class WebRTCWrapper {
                 || this.eglBase != null
                 || this.localVideoTrack != null
                 || this.remoteVideoTrack != null) {
-            final IllegalStateException e =
-                    new IllegalStateException("WebRTCWrapper hasn't been closed properly");
+            final AssertionError e =
+                    new AssertionError("WebRTCWrapper hasn't been closed properly");
             Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
             throw e;
         }
@@ -742,11 +710,10 @@ public class WebRTCWrapper {
     }
 
     public boolean applyDtmfTone(String tone) {
-        if (toneManager == null || peerConnection == null || localAudioTrack == null) {
-            return false;
-        }
         localAudioTrack.rtpSender.dtmf().insertDtmf(tone, TONE_DURATION, 100);
-        toneManager.startTone(TONE_CODES.get(tone), TONE_DURATION);
+        final var toneGenerator =
+                new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
+        toneGenerator.startTone(TONE_CODES.get(tone), TONE_DURATION);
         return true;
     }
 
@@ -799,27 +766,15 @@ public class WebRTCWrapper {
         return context;
     }
 
-    AppRTCAudioManager getAudioManager() {
-        return appRTCAudioManager;
-    }
-
     void execute(final Runnable command) {
         this.executorService.execute(command);
     }
 
-    public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) {
-        mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference));
-    }
-
     public interface EventCallback {
         void onIceCandidate(IceCandidate iceCandidate);
 
         void onConnectionChange(PeerConnection.PeerConnectionState newState);
 
-        void onAudioDeviceChanged(
-                AppRTCAudioManager.AudioDevice selectedAudioDevice,
-                Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
-
         void onRenegotiationNeeded();
     }
 

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

@@ -142,8 +142,15 @@ public class JinglePacket extends IqPacket {
         TRANSPORT_REPLACE;
 
         public static Action of(final String value) {
-            // TODO handle invalid
-            return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
+            if (Strings.isNullOrEmpty(value)) {
+                return null;
+            }
+            try {
+                return Action.valueOf(
+                        CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
+            } catch (final IllegalArgumentException e) {
+                return null;
+            }
         }
 
         @Override

src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java 🔗

@@ -31,7 +31,6 @@ public class PublishOptions {
         options.putString("pubsub#access_model", "whitelist");
         options.putString("pubsub#send_last_published_item", "never");
         options.putString("pubsub#max_items", "max");
-
         options.putString("pubsub#notify_delete", "true");
         options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract