From eca64c93621d628c9c2fb8f1130fb5f00f8cae47 Mon Sep 17 00:00:00 2001 From: Arne Date: Wed, 29 Dec 2021 22:34:29 +0100 Subject: [PATCH] removed OTR (for now..), cherry picked blabber.im updates (Christan Schneppe, Daniel Gultsch) --- .circleci/config.yml | 24 +- .github/workflows/android.yml | 34 + build.gradle | 61 +- proguard-rules.pro | 2 +- src/main/AndroidManifest.xml | 31 +- .../monocles/chat/ui/PermissionsActivity.java | 6 +- .../java/de/monocles/chat/ui/StartUI.java | 34 +- .../java/eu/siacs/conversations/Config.java | 90 +- .../android/JabberIdContact.java | 65 +- .../crypto/DomainHostnameVerifier.java | 3 +- .../conversations/crypto/OtrService.java | 312 ----- .../crypto/PgpDecryptionService.java | 2 +- .../siacs/conversations/crypto/PgpEngine.java | 32 +- .../crypto/XmppDomainVerifier.java | 2 +- .../crypto/axolotl/AxolotlService.java | 218 ++- .../siacs/conversations/entities/Account.java | 34 - .../siacs/conversations/entities/Contact.java | 26 - .../conversations/entities/Conversation.java | 281 ++-- .../eu/siacs/conversations/entities/Edit.java | 36 +- .../entities/IndividualMessage.java | 7 +- .../siacs/conversations/entities/Message.java | 204 +-- .../conversations/entities/MucOptions.java | 18 +- .../entities/ServiceDiscoveryResult.java | 4 +- .../generator/AbstractGenerator.java | 25 +- .../generator/MessageGenerator.java | 43 +- .../generator/PresenceGenerator.java | 2 +- .../http/HttpConnectionManager.java | 8 +- .../http/HttpDownloadConnection.java | 11 +- .../http/HttpUploadConnection.java | 34 +- .../eu/siacs/conversations/http/Method.java | 2 +- .../conversations/http/SlotRequester.java | 2 +- .../conversations/parser/AbstractParser.java | 26 +- .../siacs/conversations/parser/IqParser.java | 2 +- .../conversations/parser/MessageParser.java | 217 ++- .../conversations/parser/PresenceParser.java | 8 +- .../persistance/DatabaseBackend.java | 104 +- .../persistance/FileBackend.java | 244 ++-- .../services/AbstractConnectionManager.java | 14 +- .../AttachFileToConversationRunnable.java | 83 +- .../conversations/services/AudioPlayer.java | 14 +- .../services/ChannelDiscoveryService.java | 2 +- .../services/ExportBackupService.java | 105 +- .../services/ImportBackupService.java | 9 +- .../services/MemorizingTrustManager.java | 7 +- .../services/MessageArchiveService.java | 46 +- .../services/NotificationService.java | 41 +- .../services/ProviderService.java | 117 ++ .../conversations/services/UpdateService.java | 227 ++++ .../services/XmppConnectionService.java | 292 ++-- .../siacs/conversations/ui/AboutActivity.java | 1 - .../conversations/ui/AboutPreference.java | 13 +- .../ui/ConferenceDetailsActivity.java | 36 +- .../ui/ContactDetailsActivity.java | 49 +- .../ui/ConversationFragment.java | 632 ++++++--- .../ui/ConversationsActivity.java | 341 +++-- .../ui/ConversationsOverviewFragment.java | 26 +- .../ui/EasyOnboardingInviteActivity.java | 3 +- .../conversations/ui/EditAccountActivity.java | 36 +- .../ui/ImportBackupActivity.java | 4 +- .../conversations/ui/LocationActivity.java | 279 +++- .../conversations/ui/MagicCreateActivity.java | 24 +- .../ui/ManageAccountActivity.java | 7 +- .../ui/MemoryManagementActivity.java | 36 +- .../conversations/ui/MucUsersActivity.java | 2 +- .../conversations/ui/RecordingActivity.java | 2 +- .../conversations/ui/RtpSessionActivity.java | 78 +- .../siacs/conversations/ui/ScanActivity.java | 10 +- .../conversations/ui/SearchActivity.java | 54 +- .../conversations/ui/SetSettingsActivity.java | 79 +- .../conversations/ui/SettingsActivity.java | 26 +- .../ui/ShareLocationActivity.java | 348 +++-- .../conversations/ui/ShareWithActivity.java | 8 +- .../ui/ShowLocationActivity.java | 303 +++-- .../ui/StartConversationActivity.java | 48 +- .../conversations/ui/UpdaterActivity.java | 443 ++++++ .../conversations/ui/UriHandlerActivity.java | 178 ++- .../conversations/ui/VerifyOTRActivity.java | 450 ------- .../conversations/ui/WelcomeActivity.java | 14 +- .../siacs/conversations/ui/XmppActivity.java | 206 ++- .../ui/adapter/ConversationAdapter.java | 40 +- .../ui/adapter/MediaAdapter.java | 2 - .../ui/adapter/MediaPreviewAdapter.java | 20 + .../ui/adapter/MessageAdapter.java | 237 ++-- .../ui/adapter/MessageLogAdapter.java | 97 ++ .../conversations/ui/adapter/UserAdapter.java | 4 +- .../ui/adapter/UserPreviewAdapter.java | 1 - .../ui/adapter/model/MessageLogModel.java | 21 + .../conversations/ui/util/ActionBarUtil.java | 114 ++ .../conversations/ui/util/Attachment.java | 11 +- .../ui/util/ConversationMenuConfigurator.java | 1 - .../conversations/ui/util/CustomTab.java | 6 +- .../conversations/ui/util/GridManager.java | 5 +- .../conversations/ui/util/KeyboardUtils.java | 121 ++ .../conversations/ui/util/LocationHelper.java | 69 + .../conversations/ui/util/MyLinkify.java | 171 ++- .../conversations/ui/util/QuoteHelper.java | 113 +- .../conversations/ui/util/Rationals.java | 2 +- .../conversations/ui/util/ShareUtil.java | 6 +- .../conversations/ui/util/UpdateHelper.java | 16 +- .../conversations/ui/util/UriHelper.java | 30 + .../conversations/ui/widget/EditMessage.java | 4 +- .../siacs/conversations/ui/widget/Marker.java | 54 + .../conversations/ui/widget/MyLocation.java | 52 + .../conversations/ui/widget/RichLinkView.java | 8 +- .../ui/widget/SurfaceViewRenderer.java | 2 +- .../conversations/utils/Compatibility.java | 22 +- .../utils/EasyOnboardingInvite.java | 10 +- .../siacs/conversations/utils/ExifHelper.java | 161 --- .../siacs/conversations/utils/GeoHelper.java | 8 +- .../conversations/utils/ImStyleParser.java | 3 +- .../conversations/utils/LocationHelper.java | 4 +- .../conversations/utils/LocationProvider.java | 75 ++ .../conversations/utils/MessageUtils.java | 26 +- .../siacs/conversations/utils/MimeUtils.java | 82 +- .../siacs/conversations/utils/Namespace.java | 1 + .../siacs/conversations/utils/Patterns.java | 76 +- .../conversations/utils/PhoneHelper.java | 14 + .../conversations/utils/RichPreview.java | 12 +- .../utils/SocksSocketFactory.java | 48 +- .../conversations/utils/StylingHelper.java | 16 +- .../conversations/utils/TimeFrameUtils.java | 12 +- .../utils/TranscoderStrategies.java | 38 + .../siacs/conversations/utils/UIHelper.java | 92 +- .../eu/siacs/conversations/utils/XmppUri.java | 17 +- .../eu/siacs/conversations/xml/Element.java | 17 +- .../conversations/xmpp/XmppConnection.java | 42 +- .../conversations/xmpp/jid/OtrJidHelper.java | 9 +- .../xmpp/jingle/JingleConnectionManager.java | 2 +- .../xmpp/jingle/JingleRtpConnection.java | 730 +++++++--- .../xmpp/jingle/JingleSocks5Transport.java | 34 +- .../xmpp/jingle/OmemoVerification.java | 2 +- .../xmpp/jingle/RtpContentMap.java | 65 +- .../xmpp/jingle/RtpEndUserState.java | 3 +- .../xmpp/jingle/SessionDescription.java | 5 +- .../xmpp/jingle/ToneManager.java | 2 +- .../xmpp/jingle/WebRTCWrapper.java | 172 +-- .../jingle/stanzas/IceUdpTransportInfo.java | 90 +- .../xmpp/jingle/stanzas/Reason.java | 13 + .../AbstractAcknowledgeableStanza.java | 6 +- src/main/res/anim/dft.xml | 8 + src/main/res/anim/ufb.xml | 8 + src/main/res/color/button_state_color.xml | 6 +- .../res/color/text_input_stroke_color.xml | 7 + src/main/res/drawable-hdpi/ic_close_black.png | Bin 0 -> 236 bytes src/main/res/drawable-hdpi/ic_close_white.png | Bin 0 -> 288 bytes .../drawable-hdpi/ic_format_bold_black.png | Bin 0 -> 313 bytes .../drawable-hdpi/ic_format_bold_white.png | Bin 0 -> 300 bytes .../drawable-hdpi/ic_format_italic_black.png | Bin 0 -> 262 bytes .../drawable-hdpi/ic_format_italic_white.png | Bin 0 -> 255 bytes .../ic_format_monospace_black.png | Bin 0 -> 212 bytes .../ic_format_monospace_white.png | Bin 0 -> 214 bytes .../ic_format_strikethrough_black.png | Bin 0 -> 378 bytes .../ic_format_strikethrough_white.png | Bin 0 -> 379 bytes .../drawable-hdpi/ic_gps_fixed_black_24dp.png | Bin 0 -> 698 bytes .../drawable-hdpi/ic_gps_fixed_white_24dp.png | Bin 0 -> 728 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 0 -> 592 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 0 -> 639 bytes src/main/res/drawable-hdpi/ic_help_black.png | Bin 0 -> 532 bytes src/main/res/drawable-hdpi/ic_help_white.png | Bin 0 -> 526 bytes src/main/res/drawable-hdpi/marker.png | Bin 0 -> 1740 bytes src/main/res/drawable-mdpi/ic_close_black.png | Bin 0 -> 205 bytes src/main/res/drawable-mdpi/ic_close_white.png | Bin 0 -> 200 bytes .../drawable-mdpi/ic_delete_black_18dp.png | Bin 0 -> 383 bytes .../drawable-mdpi/ic_delete_white_18dp.png | Bin 0 -> 390 bytes .../drawable-mdpi/ic_format_bold_black.png | Bin 0 -> 228 bytes .../drawable-mdpi/ic_format_bold_white.png | Bin 0 -> 217 bytes .../drawable-mdpi/ic_format_italic_black.png | Bin 0 -> 205 bytes .../drawable-mdpi/ic_format_italic_white.png | Bin 0 -> 197 bytes .../ic_format_monospace_black.png | Bin 0 -> 179 bytes .../ic_format_monospace_white.png | Bin 0 -> 174 bytes .../ic_format_strikethrough_black.png | Bin 0 -> 275 bytes .../ic_format_strikethrough_white.png | Bin 0 -> 274 bytes .../drawable-mdpi/ic_gps_fixed_black_24dp.png | Bin 0 -> 398 bytes .../drawable-mdpi/ic_gps_fixed_white_24dp.png | Bin 0 -> 420 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 0 -> 353 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 0 -> 376 bytes src/main/res/drawable-mdpi/ic_help_black.png | Bin 0 -> 336 bytes src/main/res/drawable-mdpi/ic_help_white.png | Bin 0 -> 345 bytes src/main/res/drawable-mdpi/marker.png | Bin 0 -> 1133 bytes .../res/drawable-xhdpi/ic_close_black.png | Bin 0 -> 284 bytes .../res/drawable-xhdpi/ic_close_white.png | Bin 0 -> 283 bytes .../drawable-xhdpi/ic_delete_black_18dp.png | Bin 0 -> 427 bytes .../drawable-xhdpi/ic_delete_white_18dp.png | Bin 0 -> 433 bytes .../drawable-xhdpi/ic_format_bold_black.png | Bin 0 -> 400 bytes .../drawable-xhdpi/ic_format_bold_white.png | Bin 0 -> 378 bytes .../drawable-xhdpi/ic_format_italic_black.png | Bin 0 -> 292 bytes .../drawable-xhdpi/ic_format_italic_white.png | Bin 0 -> 287 bytes .../ic_format_monospace_black.png | Bin 0 -> 289 bytes .../ic_format_monospace_white.png | Bin 0 -> 279 bytes .../ic_format_strikethrough_black.png | Bin 0 -> 498 bytes .../ic_format_strikethrough_white.png | Bin 0 -> 482 bytes .../ic_gps_fixed_black_24dp.png | Bin 0 -> 871 bytes .../ic_gps_fixed_white_24dp.png | Bin 0 -> 936 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 0 -> 754 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 0 -> 796 bytes src/main/res/drawable-xhdpi/ic_help_black.png | Bin 0 -> 668 bytes src/main/res/drawable-xhdpi/ic_help_white.png | Bin 0 -> 670 bytes src/main/res/drawable-xhdpi/marker.png | Bin 0 -> 2328 bytes .../res/drawable-xxhdpi/ic_close_black.png | Bin 0 -> 393 bytes .../res/drawable-xxhdpi/ic_close_white.png | Bin 0 -> 575 bytes .../drawable-xxhdpi/ic_delete_black_18dp.png | Bin 0 -> 496 bytes .../drawable-xxhdpi/ic_delete_white_18dp.png | Bin 0 -> 515 bytes .../drawable-xxhdpi/ic_format_bold_black.png | Bin 0 -> 614 bytes .../drawable-xxhdpi/ic_format_bold_white.png | Bin 0 -> 594 bytes .../ic_format_italic_black.png | Bin 0 -> 393 bytes .../ic_format_italic_white.png | Bin 0 -> 384 bytes .../ic_format_monospace_black.png | Bin 0 -> 458 bytes .../ic_format_monospace_white.png | Bin 0 -> 567 bytes .../ic_format_strikethrough_black.png | Bin 0 -> 718 bytes .../ic_format_strikethrough_white.png | Bin 0 -> 707 bytes .../ic_gps_fixed_black_24dp.png | Bin 0 -> 1660 bytes .../ic_gps_fixed_white_24dp.png | Bin 0 -> 1774 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 0 -> 1397 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 0 -> 1490 bytes .../res/drawable-xxhdpi/ic_help_black.png | Bin 0 -> 999 bytes .../res/drawable-xxhdpi/ic_help_white.png | Bin 0 -> 1025 bytes src/main/res/drawable-xxhdpi/marker.png | Bin 0 -> 3794 bytes .../drawable-xxxhdpi/ic_delete_black_18dp.png | Bin 0 -> 510 bytes .../drawable-xxxhdpi/ic_delete_white_18dp.png | Bin 0 -> 544 bytes .../ic_gps_fixed_black_24dp.png | Bin 0 -> 1977 bytes .../ic_gps_fixed_white_24dp.png | Bin 0 -> 2185 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 0 -> 1657 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 0 -> 1831 bytes .../ic_link_off_white_24dp.png | Bin 0 -> 2832 bytes .../drawable-xxxhdpi/ic_link_white_24dp.png | Bin 0 -> 786 bytes src/main/res/drawable-xxxhdpi/marker.png | Bin 0 -> 5177 bytes src/main/res/drawable/white_cursor.xml | 2 +- src/main/res/layout/account_row.xml | 2 +- src/main/res/layout/activity_about.xml | 2 + .../res/layout/activity_change_password.xml | 8 +- .../res/layout/activity_channel_discovery.xml | 4 +- .../res/layout/activity_choose_contact.xml | 3 +- .../res/layout/activity_contact_details.xml | 14 +- src/main/res/layout/activity_easy_invite.xml | 23 +- src/main/res/layout/activity_edit_account.xml | 115 +- .../res/layout/activity_import_backup.xml | 4 +- src/main/res/layout/activity_magic_create.xml | 24 +- .../res/layout/activity_manage_accounts.xml | 6 +- src/main/res/layout/activity_media_viewer.xml | 11 +- .../res/layout/activity_memory_management.xml | 16 +- src/main/res/layout/activity_muc_details.xml | 43 +- src/main/res/layout/activity_muc_users.xml | 4 +- .../activity_publish_profile_picture.xml | 2 +- src/main/res/layout/activity_recording.xml | 4 +- src/main/res/layout/activity_rtp_session.xml | 39 +- src/main/res/layout/activity_set_settings.xml | 72 +- .../res/layout/activity_share_locaction.xml | 118 +- .../res/layout/activity_show_locaction.xml | 36 - .../res/layout/activity_show_location.xml | 62 + .../layout/activity_start_conversation.xml | 3 +- src/main/res/layout/activity_trust_keys.xml | 10 +- src/main/res/layout/activity_uri_handler.xml | 31 + src/main/res/layout/choose_account_dialog.xml | 2 +- src/main/res/layout/contact.xml | 2 +- src/main/res/layout/conversation_list_row.xml | 8 +- .../res/layout/create_conference_dialog.xml | 3 +- .../layout/create_public_channel_dialog.xml | 6 +- src/main/res/layout/dialog_enter_password.xml | 1 + .../res/layout/dialog_join_conference.xml | 8 +- src/main/res/layout/dialog_presence.xml | 9 +- src/main/res/layout/dialog_quickedit.xml | 5 +- src/main/res/layout/enter_jid_dialog.xml | 6 +- src/main/res/layout/fragment_conversation.xml | 91 +- .../fragment_conversations_overview.xml | 3 +- src/main/res/layout/keys_card.xml | 4 +- src/main/res/layout/message_content.xml | 67 +- src/main/res/layout/message_date_bubble.xml | 5 +- src/main/res/layout/message_log_item.xml | 40 + src/main/res/layout/message_received.xml | 24 +- src/main/res/layout/message_rtp_session.xml | 2 +- src/main/res/layout/message_sent.xml | 12 +- src/main/res/layout/message_status.xml | 2 +- src/main/res/menu/activity_conversations.xml | 2 - src/main/res/menu/easy_onboarding_invite.xml | 2 +- src/main/res/menu/menu_show_location.xml | 15 + src/main/res/menu/message_context.xml | 14 +- src/main/res/raw/countries | 245 ++++ src/main/res/values-ar-rSA/strings.xml | 796 +++++++++++ src/main/res/values-az-rAZ/strings.xml | 1144 ++++++++++++++++ src/main/res/values-bg-rBG/strings.xml | 831 ++++++++++++ src/main/res/values-ca-rES/strings.xml | 1146 ++++++++++++++++ src/main/res/values-ceb-rPH/strings.xml | 508 +++++++ src/main/res/values-cs-rCZ/strings.xml | 816 +++++++++++ src/main/res/values-de-rDE/strings.xml | 1170 ++++++++++++++++ src/main/res/values-el-rGR/strings.xml | 266 ++++ src/main/res/values-es-rES/strings.xml | 1170 ++++++++++++++++ src/main/res/values-eu-rES/strings.xml | 501 +++++++ src/main/res/values-fil-rPH/strings.xml | 534 ++++++++ src/main/res/values-fr-rFR/strings.xml | 1159 ++++++++++++++++ src/main/res/values-gl-rES/strings.xml | 1146 ++++++++++++++++ src/main/res/values-in-rID/strings.xml | 553 ++++++++ src/main/res/values-it-rIT/strings.xml | 1161 ++++++++++++++++ src/main/res/values-ja-rJP/strings.xml | 1133 ++++++++++++++++ src/main/res/values-kn-rIN/strings.xml | 379 ++++++ src/main/res/values-nl-rNL/strings.xml | 1170 ++++++++++++++++ src/main/res/values-pl-rPL/strings.xml | 1042 ++++++++++++++ src/main/res/values-pt-rBR/strings.xml | 857 ++++++++++++ src/main/res/values-pt-rPT/strings.xml | 361 +++++ src/main/res/values-ro-rRO/strings.xml | 1182 ++++++++++++++++ src/main/res/values-ru-rRU/strings.xml | 1192 +++++++++++++++++ src/main/res/values-sv-rSE/strings.xml | 434 ++++++ src/main/res/values-th-rTH/strings.xml | 16 + src/main/res/values-tl-rPH/strings.xml | 84 ++ src/main/res/values-tr-rTR/strings.xml | 960 +++++++++++++ src/main/res/values-uk-rUA/strings.xml | 1029 ++++++++++++++ src/main/res/values-zh-rCN/strings.xml | 1066 +++++++++++++++ src/main/res/values-zh-rTW/strings.xml | 910 +++++++++++++ src/main/res/values/about.xml | 12 +- src/main/res/values/attrs.xml | 16 +- src/main/res/values/defaults.xml | 153 +-- src/main/res/values/strings.xml | 172 +-- src/main/res/values/styles.xml | 22 +- src/main/res/values/themes.xml | 14 + src/main/res/xml/file_paths.xml | 3 + src/main/res/xml/preferences.xml | 10 +- 315 files changed, 31885 insertions(+), 4561 deletions(-) create mode 100644 .github/workflows/android.yml delete mode 100644 src/main/java/eu/siacs/conversations/crypto/OtrService.java create mode 100644 src/main/java/eu/siacs/conversations/services/ProviderService.java create mode 100644 src/main/java/eu/siacs/conversations/services/UpdateService.java create mode 100644 src/main/java/eu/siacs/conversations/ui/UpdaterActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java create mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/MessageLogAdapter.java create mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/model/MessageLogModel.java create mode 100644 src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java create mode 100644 src/main/java/eu/siacs/conversations/ui/util/KeyboardUtils.java create mode 100644 src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java create mode 100644 src/main/java/eu/siacs/conversations/ui/util/UriHelper.java create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/Marker.java create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java create mode 100644 src/main/java/eu/siacs/conversations/utils/LocationProvider.java create mode 100644 src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java create mode 100644 src/main/res/anim/dft.xml create mode 100644 src/main/res/anim/ufb.xml create mode 100644 src/main/res/color/text_input_stroke_color.xml create mode 100644 src/main/res/drawable-hdpi/ic_close_black.png create mode 100644 src/main/res/drawable-hdpi/ic_close_white.png create mode 100644 src/main/res/drawable-hdpi/ic_format_bold_black.png create mode 100644 src/main/res/drawable-hdpi/ic_format_bold_white.png create mode 100644 src/main/res/drawable-hdpi/ic_format_italic_black.png create mode 100644 src/main/res/drawable-hdpi/ic_format_italic_white.png create mode 100644 src/main/res/drawable-hdpi/ic_format_monospace_black.png create mode 100644 src/main/res/drawable-hdpi/ic_format_monospace_white.png create mode 100644 src/main/res/drawable-hdpi/ic_format_strikethrough_black.png create mode 100644 src/main/res/drawable-hdpi/ic_format_strikethrough_white.png create mode 100644 src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_gps_fixed_white_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_gps_not_fixed_white_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_help_black.png create mode 100644 src/main/res/drawable-hdpi/ic_help_white.png create mode 100644 src/main/res/drawable-hdpi/marker.png create mode 100644 src/main/res/drawable-mdpi/ic_close_black.png create mode 100644 src/main/res/drawable-mdpi/ic_close_white.png create mode 100644 src/main/res/drawable-mdpi/ic_delete_black_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_delete_white_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_format_bold_black.png create mode 100644 src/main/res/drawable-mdpi/ic_format_bold_white.png create mode 100644 src/main/res/drawable-mdpi/ic_format_italic_black.png create mode 100644 src/main/res/drawable-mdpi/ic_format_italic_white.png create mode 100644 src/main/res/drawable-mdpi/ic_format_monospace_black.png create mode 100644 src/main/res/drawable-mdpi/ic_format_monospace_white.png create mode 100644 src/main/res/drawable-mdpi/ic_format_strikethrough_black.png create mode 100644 src/main/res/drawable-mdpi/ic_format_strikethrough_white.png create mode 100644 src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_gps_not_fixed_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_help_black.png create mode 100644 src/main/res/drawable-mdpi/ic_help_white.png create mode 100644 src/main/res/drawable-mdpi/marker.png create mode 100644 src/main/res/drawable-xhdpi/ic_close_black.png create mode 100644 src/main/res/drawable-xhdpi/ic_close_white.png create mode 100644 src/main/res/drawable-xhdpi/ic_delete_black_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_delete_white_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_bold_black.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_bold_white.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_italic_black.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_italic_white.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_monospace_black.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_monospace_white.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_strikethrough_black.png create mode 100644 src/main/res/drawable-xhdpi/ic_format_strikethrough_white.png create mode 100644 src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_gps_not_fixed_white_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_help_black.png create mode 100644 src/main/res/drawable-xhdpi/ic_help_white.png create mode 100644 src/main/res/drawable-xhdpi/marker.png create mode 100644 src/main/res/drawable-xxhdpi/ic_close_black.png create mode 100644 src/main/res/drawable-xxhdpi/ic_close_white.png create mode 100644 src/main/res/drawable-xxhdpi/ic_delete_black_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_delete_white_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_bold_black.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_bold_white.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_italic_black.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_italic_white.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_monospace_black.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_monospace_white.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_strikethrough_black.png create mode 100644 src/main/res/drawable-xxhdpi/ic_format_strikethrough_white.png create mode 100644 src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_gps_fixed_white_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_gps_not_fixed_white_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_help_black.png create mode 100644 src/main/res/drawable-xxhdpi/ic_help_white.png create mode 100644 src/main/res/drawable-xxhdpi/marker.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_delete_black_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_delete_white_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_link_off_white_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_link_white_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/marker.png delete mode 100644 src/main/res/layout/activity_show_locaction.xml create mode 100644 src/main/res/layout/activity_show_location.xml create mode 100644 src/main/res/layout/activity_uri_handler.xml create mode 100644 src/main/res/layout/message_log_item.xml create mode 100644 src/main/res/menu/menu_show_location.xml create mode 100644 src/main/res/raw/countries create mode 100644 src/main/res/values-ar-rSA/strings.xml create mode 100644 src/main/res/values-az-rAZ/strings.xml create mode 100644 src/main/res/values-bg-rBG/strings.xml create mode 100644 src/main/res/values-ca-rES/strings.xml create mode 100644 src/main/res/values-ceb-rPH/strings.xml create mode 100644 src/main/res/values-cs-rCZ/strings.xml create mode 100644 src/main/res/values-de-rDE/strings.xml create mode 100644 src/main/res/values-el-rGR/strings.xml create mode 100644 src/main/res/values-es-rES/strings.xml create mode 100644 src/main/res/values-eu-rES/strings.xml create mode 100644 src/main/res/values-fil-rPH/strings.xml create mode 100644 src/main/res/values-fr-rFR/strings.xml create mode 100644 src/main/res/values-gl-rES/strings.xml create mode 100644 src/main/res/values-in-rID/strings.xml create mode 100644 src/main/res/values-it-rIT/strings.xml create mode 100644 src/main/res/values-ja-rJP/strings.xml create mode 100644 src/main/res/values-kn-rIN/strings.xml create mode 100644 src/main/res/values-nl-rNL/strings.xml create mode 100644 src/main/res/values-pl-rPL/strings.xml create mode 100644 src/main/res/values-pt-rBR/strings.xml create mode 100644 src/main/res/values-pt-rPT/strings.xml create mode 100644 src/main/res/values-ro-rRO/strings.xml create mode 100644 src/main/res/values-ru-rRU/strings.xml create mode 100644 src/main/res/values-sv-rSE/strings.xml create mode 100644 src/main/res/values-th-rTH/strings.xml create mode 100644 src/main/res/values-tl-rPH/strings.xml create mode 100644 src/main/res/values-tr-rTR/strings.xml create mode 100644 src/main/res/values-uk-rUA/strings.xml create mode 100644 src/main/res/values-zh-rCN/strings.xml create mode 100644 src/main/res/values-zh-rTW/strings.xml diff --git a/.circleci/config.yml b/.circleci/config.yml index d12316eb6..d0c9ffa55 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: - restore_cache: key: gradle-{{ checksum "build.gradle" }}-{{ checksum ".circleci/config.yml" }} - run: export GRADLE_USER_HOME=$PWD/.gradle - - run: wget -O libs/libwebrtc-m90.aar https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar + - run: wget -O libs/libwebrtc-m85.aar https://www.pix-art.de/files/libwebrtc-m85.aar - run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null - run: ./gradlew lintGitDebug - save_cache: @@ -27,7 +27,7 @@ jobs: - restore_cache: key: android - run: export GRADLE_USER_HOME=$PWD/.gradle - - run: wget -O libs/libwebrtc-m90.aar https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar + - run: wget -O libs/libwebrtc-m85.aar https://www.pix-art.de/files/libwebrtc-m85.aar - run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null # build - run: ./gradlew assembleGit @@ -46,10 +46,10 @@ jobs: - restore_cache: key: android - run: export GRADLE_USER_HOME=$PWD/.gradle - - run: wget -O libs/libwebrtc-m90.aar https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar + - run: wget -O libs/libwebrtc-m85.aar https://www.pix-art.de/files/libwebrtc-m85.aar - run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null # workaround for fdroid nightly circleci bug - - run: sed -i "s/os.getenv('CIRCLE_REPOSITORY_URL')/\"https:\/\/github.com\/kriztan\/Monocles-Messenger\"/" /usr/lib/python3/dist-packages/fdroidserver/nightly.py + - run: sed -i "s/os.getenv('CIRCLE_REPOSITORY_URL')/\"https:\/\/github.com\/kriztan\/Pix-Art-Messenger\"/" /usr/lib/python3/dist-packages/fdroidserver/nightly.py # generate version number - run: sed -i "s/^\(\s*versionCode\s*\).*$/\1$(git rev-list --first-parent --count HEAD)/" build.gradle - run: sed -i "0,/versionName/s/^\(\s*versionName\).*/\1 \"$(printf '%s-%05d' $(git describe --tag --abbrev=0) $(git rev-list --first-parent --count HEAD))\"/" build.gradle @@ -67,11 +67,11 @@ workflows: version: 2 test_build: jobs: - - build: - filters: - branches: - ignore: master - - publish: - filters: - branches: - only: master + - build: + filters: + branches: + ignore: master + - publish: + filters: + branches: + only: master diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 000000000..dd1735eac --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,34 @@ +name: Android CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + - name: Download WebRTC + run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build Quicksy (Compat) + run: ./gradlew assembleQuicksyFreeCompatDebug + - name: Build Quicksy (System) + run: ./gradlew assembleQuicksyFreeSystemDebug + - name: Build Conversations (Compat) + run: ./gradlew assembleConversationsFreeCompatDebug + - name: Build Conversations (System) + run: ./gradlew assembleConversationsFreeSystemDebug + + diff --git a/build.gradle b/build.gradle index 443d0a9c8..4b858ae38 100644 --- a/build.gradle +++ b/build.gradle @@ -15,19 +15,18 @@ apply plugin: 'com.android.application' allprojects { repositories { google() - jcenter() mavenCentral() maven { url "https://jitpack.io" } - maven { url "https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar" } + maven { url "https://raw.github.com/abdularis/libwebrtc-android/repo/" } } } repositories { google() mavenCentral() - jcenter() maven { url "https://jitpack.io" } - maven { url "https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar" } + maven { url "https://raw.github.com/abdularis/libwebrtc-android/repo/" } + jcenter() } configurations { @@ -37,9 +36,9 @@ configurations { } dependencies { - implementation 'org.webrtc:google-webrtc:1.+' + implementation 'com.github.webrtc-sdk:android:93.4577.01' implementation project(':libs:android-transcoder') - playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') { ///higher versions are causing crashes due to missing project IDs + playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -51,7 +50,6 @@ dependencies { exclude group: 'com.android.support', module: 'appcompat-v7' exclude group: 'com.android.support', module: 'exifinterface' } - implementation 'org.jitsi:org.otr4j:0.23' implementation 'org.bouncycastle:bcmail-jdk15on:1.64' implementation 'org.gnu.inet:libidn:1.15' implementation 'com.google.zxing:core:3.3.3' // > 3.3.x not working below SDK 24 @@ -69,28 +67,30 @@ dependencies { implementation 'androidx.emoji:emoji:1.1.0' gitImplementation 'androidx.emoji:emoji-appcompat:1.1.0' gitImplementation 'androidx.emoji:emoji-bundled:1.1.0' - implementation 'com.google.android.material:material:1.0.0' // higher versions than 1.0.0 cause strange fab design + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.cardview:cardview:1.0.0' // for compatibility implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0' implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.0' implementation 'com.wefika:flowlayout:0.4.1' implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.5' - implementation 'org.jxmpp:jxmpp-jid:1.0.1' + implementation 'org.jxmpp:jxmpp-jid:1.0.2' implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:2.5.2' - implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15' // 1.2.15 is last working version for minSDK 16 + implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23' implementation 'me.drakeet.support:toastcompat:1.1.0' - implementation 'org.osmdroid:osmdroid-android:6.1.5' - implementation 'com.leinardi.android:speed-dial:3.1.1' + implementation 'org.osmdroid:osmdroid-android:6.1.11' + implementation 'com.leinardi.android:speed-dial:3.2.0' implementation 'com.squareup.picasso:picasso:2.71828' - implementation 'com.squareup.okhttp3:okhttp:4.9.2' // versions > 3.12.x don't support API level < 21 anymore - implementation 'com.squareup.retrofit2:retrofit:2.9.0' //retrofit needs to stick with 2.6.x for SDK < 21 (https://github.com/square/retrofit/blob/master/CHANGELOG.md) + implementation 'com.squareup.okhttp3:okhttp:4.9.2' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - implementation 'com.google.guava:guava:30.1.1-android' + implementation 'com.google.guava:guava:31.0.1-android' implementation 'com.github.AppIntro:AppIntro:6.1.0' - implementation "androidx.browser:browser:1.3.0" - implementation fileTree(include: ['libwebrtc-m90.aar'], dir: 'libs') + implementation 'androidx.browser:browser:1.3.0' + implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy + implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs') } ext { @@ -100,36 +100,29 @@ ext { android { - signingConfigs { - 'monocles chat' { - - } - } + //noinspection GradleCompatible compileSdkVersion 30 defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionNameSuffix " beta_(2021-12-26)" // " beta_(XXXX-XX-XX)" // activate for beta versions + versionNameSuffix " beta_(2021-12-19)" // " beta_(XXXX-XX-XX)" // activate for beta versions versionCode 110 versionName "1.4.4" //resConfigs "en" archivesBaseName += "-$versionName" - //archivesBaseName += "$versionNameSuffix" // activate for beta versions + archivesBaseName += "$versionNameSuffix" // activate for beta versions applicationId "de.monocles.chat" multiDexEnabled true + buildConfigField("String", "LOGTAG", '"monocles chat"') buildConfigField("String", "DOMAIN_LOCK", 'null') - buildConfigField("String", "MAGIC_CREATE_DOMAIN", '"monocles.de"') buildConfigField("boolean", "SHOW_INTRO", 'true') - //buildConfigField("String", "UPDATE_URL", '"https://monocles.de/chat/update/"') + buildConfigField("String", "UPDATE_URL", '"https://monocles.de/chat/update/"') resValue "string", "applicationId", applicationId - signingConfig signingConfigs.'monocles chat' - def appName = "monocles chat" - resValue "string", "app_name", appName - buildConfigField "String", "APP_NAME", "\"$appName\""; + resValue "string", "app_name", "monocles chat" } dataBinding { @@ -159,13 +152,10 @@ android { applicationId "de.monocles.chat" buildConfigField("boolean", "SHOW_MIGRATION_INFO", 'false') resValue "string", "applicationId", applicationId - signingConfig signingConfigs.'monocles chat' } git { dimension "distribution" buildConfigField("boolean", "SHOW_MIGRATION_INFO", 'true') - applicationId 'de.monocles.chat' - signingConfig signingConfigs.'monocles chat' } } if (project.hasProperty('mStoreFile') && @@ -205,7 +195,8 @@ android { lintOptions { error 'StringFormatInvalid' , 'StringFormatMatches' disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource', 'RestrictedApi' - abortOnError true + checkReleaseBuilds true + abortOnError false } subprojects { @@ -226,4 +217,4 @@ android { exclude 'META-INF/BCKEY.DSA' exclude 'META-INF/BCKEY.SF' } -} \ No newline at end of file +} diff --git a/proguard-rules.pro b/proguard-rules.pro index 30b755906..2611fbb78 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -1,6 +1,7 @@ -dontobfuscate -keep class de.monocles.chat.** +-keep class de.pixart.messenger.** -keep class eu.siacs.conversations.** -keep class org.whispersystems.** -keep class com.kyleduo.switchbutton.Configuration @@ -38,7 +39,6 @@ -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE - -keepclassmembers class eu.siacs.conversations.http.services.** { !transient ; } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 942bf4379..43111b4e2 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ - + @@ -81,9 +81,7 @@ - + @@ -92,7 +90,7 @@ android:scheme="package" /> - + + + + + + + + + - - + DOMAINS = Arrays.asList( + "conversations.im", + "zp1.net" + ); + + public static final List BLACKLISTED_DOMAINS = Arrays.asList( + "blabber.im" + ); + + public static String getRandomServer() { + try { + new ProviderService().execute(); + final String domain = ProviderService.getProviders().get(new Random().nextInt(ProviderService.getProviders().size())); + Log.d(LOGTAG, "MagicCreate account on domain: " + domain); + return domain; + } catch (Exception e) { + Log.d(LOGTAG, "Error getting random server ", e); + } + return "zp1.net"; + } + } + private Config() { } + + + public static final class Map { + public final static double INITIAL_ZOOM_LEVEL = 4; + public final static double FINAL_ZOOM_LEVEL = 15; + public final static int MY_LOCATION_INDICATOR_SIZE = 15; + public final static int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 5; + public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms + public final static float LOCATION_FIX_SPACE_DELTA = 10; // m + public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms + } + + // How deep nested quotes should be displayed. '2' means one quote nested in another. + public static final int QUOTE_MAX_DEPTH = 7; + // How deep nested quotes should be created on quoting a message. + public static final int QUOTING_MAX_DEPTH = 1; } diff --git a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java index 8b607637a..8735fbe68 100644 --- a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java +++ b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java @@ -17,6 +17,21 @@ import eu.siacs.conversations.xmpp.Jid; public class JabberIdContact extends AbstractPhoneContact { + private static final String[] PROJECTION = new String[]{ContactsContract.Data._ID, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Data.PHOTO_URI, + ContactsContract.Data.LOOKUP_KEY, + ContactsContract.CommonDataKinds.Im.DATA + }; + private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))"; + + private static final String[] SELECTION_ARGS = { + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE, + String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER), + String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM), + "xmpp" + }; + private final Jid jid; private JabberIdContact(Cursor cursor) throws IllegalArgumentException { @@ -38,38 +53,26 @@ public class JabberIdContact extends AbstractPhoneContact { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { return Collections.emptyMap(); } - final String[] PROJECTION = new String[]{ContactsContract.Data._ID, - ContactsContract.Data.DISPLAY_NAME, - ContactsContract.Data.PHOTO_URI, - ContactsContract.Data.LOOKUP_KEY, - ContactsContract.CommonDataKinds.Im.DATA}; - - final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\"" - + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE - + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL - + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER - + "\")"; - final Cursor cursor; - try { - cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null); - } catch (Exception e) { + try (final Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, SELECTION_ARGS, null)) { + if (cursor == null) { + return Collections.emptyMap(); + } + final HashMap contacts = new HashMap<>(); + while (cursor.moveToNext()) { + try { + final JabberIdContact contact = new JabberIdContact(cursor); + final JabberIdContact preexisting = contacts.put(contact.getJid(), contact); + if (preexisting == null || preexisting.rating() < contact.rating()) { + contacts.put(contact.getJid(), contact); + } + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, "unable to create jabber id contact"); + } + } + return contacts; + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to query", e); return Collections.emptyMap(); } - final HashMap contacts = new HashMap<>(); - while (cursor != null && cursor.moveToNext()) { - try { - final JabberIdContact contact = new JabberIdContact(cursor); - final JabberIdContact preexisting = contacts.put(contact.getJid(), contact); - if (preexisting == null || preexisting.rating() < contact.rating()) { - contacts.put(contact.getJid(), contact); - } - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG,"unable to create jabber id contact"); - } - } - if (cursor != null) { - cursor.close(); - } - return contacts; } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java b/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java index 2dabf5a34..402ca4f0b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java +++ b/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java @@ -1,10 +1,11 @@ package eu.siacs.conversations.crypto; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; public interface DomainHostnameVerifier extends HostnameVerifier { boolean verify(String domain, String hostname, SSLSession sslSession) throws SSLPeerUnverifiedException; + } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrService.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java deleted file mode 100644 index c7e718956..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/OtrService.java +++ /dev/null @@ -1,312 +0,0 @@ -package eu.siacs.conversations.crypto; - -import android.util.Log; - -import net.java.otr4j.OtrEngineHost; -import net.java.otr4j.OtrException; -import net.java.otr4j.OtrPolicy; -import net.java.otr4j.OtrPolicyImpl; -import net.java.otr4j.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.FragmenterInstructions; -import net.java.otr4j.session.InstanceTag; -import net.java.otr4j.session.SessionID; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.DSAPrivateKeySpec; -import java.security.spec.DSAPublicKeySpec; -import java.security.spec.InvalidKeySpecException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.generator.MessageGenerator; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jid.OtrJidHelper; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - -public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { - - private Account account; - private OtrPolicy otrPolicy; - private KeyPair keyPair; - private XmppConnectionService mXmppConnectionService; - - public OtrService(XmppConnectionService service, Account account) { - this.account = account; - this.otrPolicy = new OtrPolicyImpl(); - this.otrPolicy.setAllowV1(false); - this.otrPolicy.setAllowV2(true); - this.otrPolicy.setAllowV3(true); - this.keyPair = loadKey(account.getKeys()); - this.mXmppConnectionService = service; - } - - private KeyPair loadKey(final JSONObject keys) { - if (keys == null) { - return null; - } - synchronized (keys) { - try { - BigInteger x = new BigInteger(keys.getString("otr_x"), 16); - BigInteger y = new BigInteger(keys.getString("otr_y"), 16); - BigInteger p = new BigInteger(keys.getString("otr_p"), 16); - BigInteger q = new BigInteger(keys.getString("otr_q"), 16); - BigInteger g = new BigInteger(keys.getString("otr_g"), 16); - KeyFactory keyFactory = KeyFactory.getInstance("DSA"); - DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g); - DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); - PublicKey publicKey = keyFactory.generatePublic(pubKeySpec); - PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); - return new KeyPair(publicKey, privateKey); - } catch (JSONException e) { - return null; - } catch (NoSuchAlgorithmException e) { - return null; - } catch (InvalidKeySpecException e) { - return null; - } - } - } - - private void saveKey() { - PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = keyPair.getPrivate(); - KeyFactory keyFactory; - try { - keyFactory = KeyFactory.getInstance("DSA"); - DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec( - privateKey, DSAPrivateKeySpec.class); - DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, - DSAPublicKeySpec.class); - this.account.setKey("otr_x", privateKeySpec.getX().toString(16)); - this.account.setKey("otr_g", privateKeySpec.getG().toString(16)); - this.account.setKey("otr_p", privateKeySpec.getP().toString(16)); - this.account.setKey("otr_q", privateKeySpec.getQ().toString(16)); - this.account.setKey("otr_y", publicKeySpec.getY().toString(16)); - } catch (final NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (final InvalidKeySpecException e) { - e.printStackTrace(); - } - - } - - @Override - public void askForSecret(SessionID id, InstanceTag instanceTag, String question) { - try { - final Jid jid = OtrJidHelper.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account, jid); - if (conversation != null) { - conversation.smp().hint = question; - conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED; - mXmppConnectionService.updateConversationUi(); - } - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": smp in invalid session " + id.toString()); - } - } - - @Override - public void finishedSessionMessage(SessionID arg0, String arg1) - throws OtrException { - - } - - @Override - public String getFallbackMessage(SessionID arg0) { - return MessageGenerator.OTR_FALLBACK_MESSAGE; - } - - @Override - public byte[] getLocalFingerprintRaw(SessionID arg0) { - try { - return getFingerprintRaw(getPublicKey()); - } catch (OtrCryptoException e) { - return null; - } - } - - public PublicKey getPublicKey() { - if (this.keyPair == null) { - return null; - } - return this.keyPair.getPublic(); - } - - @Override - public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { - if (this.keyPair == null) { - KeyPairGenerator kg; - try { - kg = KeyPairGenerator.getInstance("DSA"); - this.keyPair = kg.genKeyPair(); - this.saveKey(); - mXmppConnectionService.databaseBackend.updateAccount(account); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, - "error generating key pair " + e.getMessage()); - } - } - return this.keyPair; - } - - @Override - public String getReplyForUnreadableMessage(SessionID arg0) { - // TODO Auto-generated method stub - return null; - } - - @Override - public OtrPolicy getSessionPolicy(SessionID arg0) { - return otrPolicy; - } - - @Override - public void injectMessage(SessionID session, String body) - throws OtrException { - MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getJid()); - if (session.getUserID().isEmpty()) { - packet.setAttribute("to", session.getAccountID()); - } else { - packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); - } - packet.setBody(body); - MessageGenerator.addMessageHints(packet); - try { - Jid jid = OtrJidHelper.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account, jid); - if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { - if (mXmppConnectionService.sendChatStates()) { - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - } - } - } catch (final IllegalArgumentException ignored) { - - } - - packet.setType(MessagePacket.TYPE_CHAT); - packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0"); - account.getXmppConnection().sendMessagePacket(packet); - } - - @Override - public void messageFromAnotherInstanceReceived(SessionID session) { - sendOtrErrorMessage(session, "Message from another OTR-instance received"); - } - - @Override - public void multipleInstancesDetected(SessionID arg0) { - // TODO Auto-generated method stub - - } - - @Override - public void requireEncryptedMessage(SessionID arg0, String arg1) - throws OtrException { - // TODO Auto-generated method stub - - } - - @Override - public void showError(SessionID arg0, String arg1) throws OtrException { - Log.d(Config.LOGTAG, "show error"); - } - - @Override - public void smpAborted(SessionID id) throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - private void setSmpStatus(SessionID id, int status) { - try { - final Jid jid = OtrJidHelper.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account, jid); - if (conversation != null) { - conversation.smp().status = status; - mXmppConnectionService.updateConversationUi(); - } - } catch (final IllegalArgumentException ignored) { - - } - } - - @Override - public void smpError(SessionID id, int arg1, boolean arg2) - throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - @Override - public void unencryptedMessageReceived(SessionID arg0, String arg1) - throws OtrException { - throw new OtrException(new Exception("unencrypted message received")); - } - - @Override - public void unreadableMessageReceived(SessionID session) throws OtrException { - Log.d(Config.LOGTAG, "unreadable message received"); - sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message"); - } - - public void sendOtrErrorMessage(SessionID session, String errorText) { - try { - Jid jid = OtrJidHelper.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account, jid); - String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId(); - if (id != null) { - MessagePacket packet = mXmppConnectionService.getMessageGenerator() - .generateOtrError(jid, id, errorText); - packet.setFrom(account.getJid()); - mXmppConnectionService.sendMessagePacket(account, packet); - Log.d(Config.LOGTAG, packet.toString()); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": unreadable OTR message in " + conversation.getName()); - } - } catch (IllegalArgumentException e) { - return; - } - } - - @Override - public void unverify(SessionID id, String arg1) { - setSmpStatus(id, Conversation.Smp.STATUS_FAILED); - } - - @Override - public void verify(SessionID id, String fingerprint, boolean approved) { - Log.d(Config.LOGTAG, "OtrService.verify(" + id.toString() + "," + fingerprint + "," + String.valueOf(approved) + ")"); - try { - final Jid jid = OtrJidHelper.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account, jid); - if (conversation != null) { - if (approved) { - conversation.getContact().addOtrFingerprint(fingerprint); - } - conversation.smp().hint = null; - conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final IllegalArgumentException ignored) { - } - } - - @Override - public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) { - return null; - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index 46f7e0079..acc507503 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -209,7 +209,7 @@ public class PgpDecryptionService { } } final String url = message.getFileParams().url; - mXmppConnectionService.getFileBackend().updateFileParams(message, url.toString()); + mXmppConnectionService.getFileBackend().updateFileParams(message, url.toString()); message.setEncryption(Message.ENCRYPTION_DECRYPTED); mXmppConnectionService.updateMessage(message); if (!inputFile.delete()) { diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 15b2bc3a7..420b96f26 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -128,22 +128,22 @@ public class PgpEngine { api.executeApiAsync(params, is, os, result -> { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - try { - os.flush(); - } catch (IOException ignored) { - //ignored - } - FileBackend.close(os); - mXmppConnectionService.sendMessage(message); - callback.success(message); - break; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); - break; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, message); - break; + try { + os.flush(); + } catch (IOException ignored) { + //ignored + } + FileBackend.close(os); + mXmppConnectionService.sendMessage(message); + callback.success(message); + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); + callback.error(R.string.openpgp_error, message); + break; } }); } catch (final IOException e) { diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java index ed17c5695..fc8ede071 100644 --- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java +++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java @@ -193,4 +193,4 @@ public class XmppDomainVerifier { return false; } } -} +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 574120ab9..1ab4d97a8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -8,7 +8,12 @@ import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.whispersystems.libsignal.IdentityKey; @@ -733,58 +738,62 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { axolotlStore.setFingerprintStatus(fingerprint, status); } - private void verifySessionWithPEP(final XmppAxolotlSession session) { + private ListenableFuture verifySessionWithPEP(final XmppAxolotlSession session) { Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); final SignalProtocolAddress address = session.getRemoteAddress(); final IdentityKey identityKey = session.getIdentityKey(); + final Jid jid; try { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.of(address.getName()), address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Pair verification = mXmppConnectionService.getIqParser().verification(packet); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { - try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - String fingerprint = session.getFingerprint(); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); - setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); - axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); - fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); - Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); - try { - final String cn = information.getString("subject_cn"); - final Jid jid = Jid.of(address.getName()); - Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); - account.getRoster().getContact(jid).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored - } - finishBuildingSessionsFromPEP(address); - return; - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not verify certificate"); - } - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "no verification found"); - } - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - }); - } catch (IllegalArgumentException e) { + jid = Jid.of(address.getName()); + } catch (final IllegalArgumentException e) { fetchStatusMap.put(address, FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); + return Futures.immediateFuture(session); } + final SettableFuture future = SettableFuture.create(); + final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { + Pair verification = mXmppConnectionService.getIqParser().verification(response); + if (verification != null) { + try { + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initVerify(verification.first[0]); + verifier.update(identityKey.serialize()); + if (verifier.verify(verification.second)) { + try { + mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); + String fingerprint = session.getFingerprint(); + Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); + setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); + axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); + fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid1 = Jid.of(address.getName()); + Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn); + account.getRoster().getContact(jid1).setCommonName(cn); + } catch (final IllegalArgumentException ignored) { + //ignored + } + finishBuildingSessionsFromPEP(address); + future.set(session); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not verify certificate"); + } + } + } catch (Exception e) { + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); + } + } else { + Log.d(Config.LOGTAG, "no verification found"); + } + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + future.set(session); + }); + return future; } private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { @@ -900,22 +909,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } } - private void buildSessionFromPEP(final SignalProtocolAddress address) { - buildSessionFromPEP(address, null); + private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address) { + return buildSessionFromPEP(address, null); } - private void buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { + private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { + final SettableFuture sessionSettableFuture = SettableFuture.create(); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); if (address.equals(getOwnAxolotlAddress())) { throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); } - final Jid jid = Jid.of(address.getName()); final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> { if (packet.getType() == IqPacket.TYPE.TIMEOUT) { fetchStatusMap.put(address, FetchStatus.TIMEOUT); + sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout")); } else if (packet.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); final IqParser parser = mXmppConnectionService.getIqParser(); @@ -928,6 +938,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callback.onSessionBuildFailed(); } + sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Invalid")); return; } Random random = new Random(); @@ -939,6 +950,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callback.onSessionBuildFailed(); } + sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found")); return; } @@ -953,7 +965,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); sessions.put(address, session); if (Config.X509_VERIFICATION) { - verifySessionWithPEP(session); //TODO; maybe inject callback in here too + sessionSettableFuture.setFuture(verifySessionWithPEP(session)); //TODO; maybe inject callback in here too } else { FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize())); FetchStatus fetchStatus; @@ -969,6 +981,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callback.onSessionBuildSuccessful(); } + sessionSettableFuture.set(session); } } catch (UntrustedIdentityException | InvalidKeyException e) { Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " @@ -981,6 +994,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callback.onSessionBuildFailed(); } + sessionSettableFuture.setException(new CryptoFailedException(e)); } } else { fetchStatusMap.put(address, FetchStatus.ERROR); @@ -994,8 +1008,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callback.onSessionBuildFailed(); } + sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Error")); } }); + return sessionSettableFuture; } private void removeFromDeviceAnnouncement(Integer id) { @@ -1228,36 +1244,63 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } - public OmemoVerifiedPayload encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) throws CryptoFailedException { - final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - final XmppAxolotlSession session = sessions.get(address); - if (session == null) { - throw new CryptoFailedException(String.format("No session found for %d", deviceId)); + public ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { + return Futures.transformAsync( + getSession(jid, deviceId), + session -> encrypt(rtpContentMap, session), + MoreExecutors.directExecutor() + ); + } + + private ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { + if (Config.REQUIRE_RTP_VERIFICATION) { + requireVerification(session); } final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); - omemoVerification.setDeviceId(deviceId); + omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); omemoVerification.setSessionFingerprint(session.getFingerprint()); for (final Map.Entry content : rtpContentMap.contents.entrySet()) { final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); - final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo = encrypt(descriptionTransport.transport, session); + final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo; + try { + encryptedTransportInfo = encrypt(descriptionTransport.transport, session); + } catch (final CryptoFailedException e) { + return Futures.immediateFailedFuture(e); + } descriptionTransportBuilder.put( content.getKey(), new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo) ); } - return new OmemoVerifiedPayload<>( - omemoVerification, - new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build()) - ); + return Futures.immediateFuture( + new OmemoVerifiedPayload<>( + omemoVerification, + new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build()) + )); } - public OmemoVerifiedPayload decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) throws CryptoFailedException { + private ListenableFuture getSession(final Jid jid, final int deviceId) { + final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + final XmppAxolotlSession session = sessions.get(address); + if (session == null) { + return buildSessionFromPEP(address); + } + return Futures.immediateFuture(session); + } + + public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); + final ImmutableList.Builder> pepVerificationFutures = new ImmutableList.Builder<>(); for (final Map.Entry content : omemoVerifiedRtpContentMap.contents.entrySet()) { final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); - final OmemoVerifiedPayload decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from); + final OmemoVerifiedPayload decryptedTransport; + try { + decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); + } catch (CryptoFailedException e) { + return Futures.immediateFailedFuture(e); + } omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), @@ -1265,13 +1308,26 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { ); } processPostponed(); - return new OmemoVerifiedPayload<>( - omemoVerification, - new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) + final ImmutableList> sessionFutures = pepVerificationFutures.build(); + return Futures.transform( + Futures.allAsList(sessionFutures), + sessions -> { + if (Config.REQUIRE_RTP_VERIFICATION) { + for (XmppAxolotlSession session : sessions) { + requireVerification(session); + } + } + return new OmemoVerifiedPayload<>( + omemoVerification, + new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) + ); + + }, + MoreExecutors.directExecutor() ); } - private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from) throws CryptoFailedException { + private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder> pepVerificationFutures) throws CryptoFailedException { final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes()); final OmemoVerification omemoVerification = new OmemoVerification(); @@ -1288,6 +1344,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (preKeyId != null) { postponedSessions.add(session); } + if (session.isFresh()) { + pepVerificationFutures.add(putFreshSession(session)); + } else if (Config.REQUIRE_RTP_VERIFICATION) { + pepVerificationFutures.add(Futures.immediateFuture(session)); + } fingerprint.setContent(plaintext.getPlaintext()); omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); omemoVerification.setSessionFingerprint(plaintext.getFingerprint()); @@ -1299,6 +1360,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return new OmemoVerifiedPayload<>(omemoVerification, transportInfo); } + private static void requireVerification(final XmppAxolotlSession session) { + if (session.getTrust().isVerified()) { + return; + } + throw new NotVerifiedException(String.format( + "session with %s was not verified", + session.getFingerprint() + )); + } + public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { executor.execute(new Runnable() { @Override @@ -1496,15 +1567,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return keyTransportMessage; } - private void putFreshSession(XmppAxolotlSession session) { + private ListenableFuture putFreshSession(XmppAxolotlSession session) { sessions.put(session); if (Config.X509_VERIFICATION) { if (session.getIdentityKey() != null) { - verifySessionWithPEP(session); + return verifySessionWithPEP(session); } else { Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); } } + return Futures.immediateFuture(session); } public enum FetchStatus { @@ -1690,4 +1762,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return payload; } } + + public static class NotVerifiedException extends SecurityException { + + public NotVerifiedException(String message) { + super(message); + } + + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 0b2bb0802..c471cabf3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -5,16 +5,10 @@ import android.database.Cursor; import android.os.SystemClock; import android.util.Log; -import net.java.otr4j.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; import com.google.common.base.Strings; import org.json.JSONException; import org.json.JSONObject; -import java.security.PublicKey; -import java.security.interfaces.DSAPublicKey; -import java.util.Locale; -import eu.siacs.conversations.crypto.OtrService; import java.util.ArrayList; import java.util.Collection; @@ -88,7 +82,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable protected String hostname = null; protected int port = 5222; protected boolean online = false; - private OtrService mOtrService = null; private String rosterVersion; private String displayName = null; private AxolotlService axolotlService = null; @@ -403,16 +396,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public void initAccountServices(final XmppConnectionService context) { - this.mOtrService = new OtrService(context, this); this.axolotlService = new AxolotlService(this, context); this.pgpDecryptionService = new PgpDecryptionService(context); if (xmppConnection != null) { xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); } } - public OtrService getOtrService() { - return this.mOtrService; - } public PgpDecryptionService getPgpDecryptionService() { return this.pgpDecryptionService; @@ -425,25 +414,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public void setXmppConnection(final XmppConnection connection) { this.xmppConnection = connection; } - public String getOtrFingerprint() { - if (this.otrFingerprint == null) { - try { - if (this.mOtrService == null) { - return null; - } - final PublicKey publicKey = this.mOtrService.getPublicKey(); - if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { - return null; - } - this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US); - return this.otrFingerprint; - } catch (final OtrCryptoException ignored) { - return null; - } - } else { - return this.otrFingerprint; - } - } public String getRosterVersion() { if (this.rosterVersion == null) { @@ -608,10 +578,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private List getFingerprints() { ArrayList fingerprints = new ArrayList<>(); - final String otr = this.getOtrFingerprint(); - if (otr != null) { - fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR, otr)); - } if (axolotlService == null) { return fingerprints; } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index f12e85b95..3f8a6bd40 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -516,32 +516,6 @@ public class Contact implements ListItem, Blockable { return avatar; } - - - public boolean deleteOtrFingerprint(String fingerprint) { - synchronized (this.keys) { - boolean success = false; - try { - if (this.keys.has("otr_fingerprints")) { - JSONArray newPrints = new JSONArray(); - JSONArray oldPrints = this.keys - .getJSONArray("otr_fingerprints"); - for (int i = 0; i < oldPrints.length(); ++i) { - if (!oldPrints.getString(i).equals(fingerprint)) { - newPrints.put(oldPrints.getString(i)); - } else { - success = true; - } - } - this.keys.put("otr_fingerprints", newPrints); - } - return success; - } catch (JSONException e) { - return false; - } - } - } - public boolean mutualPresenceSubscription() { return getOption(Options.FROM) && getOption(Options.TO); } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 4a8afcdb1..1bf810216 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1,20 +1,18 @@ package eu.siacs.conversations.entities; +import static eu.siacs.conversations.entities.Bookmark.printableValue; + import android.content.ContentValues; +import android.content.SharedPreferences; import android.database.Cursor; +import android.preference.PreferenceManager; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import net.java.otr4j.OtrException; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.SessionImpl; -import net.java.otr4j.session.SessionStatus; + import com.google.common.collect.ComparisonChain; import com.google.common.collect.Lists; -import java.security.interfaces.DSAPublicKey; -import java.util.Locale; import org.json.JSONArray; import org.json.JSONException; @@ -22,6 +20,7 @@ import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; @@ -34,13 +33,12 @@ import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.JidHelper; +import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.mam.MamReference; -import static eu.siacs.conversations.entities.Bookmark.printableValue; - public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { public static final String TABLENAME = "conversations"; @@ -84,15 +82,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private int mode; private JSONObject attributes; private Jid nextCounterpart; - private transient SessionImpl otrSession; - private transient String otrFingerprint = null; - private Smp mSmp = new Smp(); private transient MucOptions mucOptions = null; - private byte[] symmetricKey; private boolean messagesLeftOnServer = true; private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; - private String mLastReceivedOtrMessageId = null; private String mFirstMamReference = null; public Conversation(final String name, final Account account, final Jid contactJid, @@ -305,9 +298,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Message findMessageWithFileAndUuid(final String uuid) { synchronized (this.messages) { for (final Message message : this.messages) { + final Transferable transferable = message.getTransferable(); + final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); if (message.getUuid().equals(uuid) && message.getEncryption() != Message.ENCRYPTION_PGP - && (message.isFileOrImage() || message.treatAsDownloadable())) { + && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) { + return message; + } + } + } + return null; + } + + public Message findMessageWithUuid(final String uuid) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (message.getUuid().equals(uuid)) { return message; } } @@ -386,7 +392,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public void trim() { synchronized (this.messages) { final int size = messages.size(); - final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; + int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; + if (getAccount() != null && getAccount().getXmppConnection() != null && getAccount().getXmppConnection().getXmppConnectionService() != null) { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getAccount().getXmppConnection().getXmppConnectionService()); + int pagesize = Integer.parseInt(pref.getString("pagesize", String.valueOf(Config.PAGE_SIZE))); + int maxnumpages = Integer.parseInt(pref.getString("max_num_pages", String.valueOf(Config.MAX_NUM_PAGES))); + maxsize = pagesize * maxnumpages; + } if (size > maxsize) { List discards = this.messages.subList(0, size - maxsize); final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); @@ -399,17 +411,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { - synchronized (this.messages) { - for (Message message : this.messages) { - if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) - && (message.getEncryption() == encryptionType)) { - onMessageFound.onMessageFound(message); - } - } - } - } - public void findUnsentTextMessages(OnMessageFound onMessageFound) { final ArrayList results = new ArrayList<>(); synchronized (this.messages) { @@ -425,13 +426,24 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public Message findSentMessageWithUuidOrRemoteId(String id) { + return findSentMessageWithUuidOrRemoteId(id, false, false); + } + + public Message findSentMessageWithUuidOrRemoteId(String id, boolean ignorestatus, boolean withedits) { synchronized (this.messages) { for (Message message : this.messages) { - if (id.equals(message.getUuid()) - || (message.getStatus() >= Message.STATUS_SEND - && id.equals(message.getRemoteMsgId()))) { + + if (id.equals(message.getUuid()) || ((message.getStatus() >= Message.STATUS_SEND || ignorestatus) && id.equals(message.getRemoteMsgId()))) { return message; } + + if (withedits) { + for (Edit itm : message.edits) { + if (id.equals(itm.getEditedId())) { + return message; + } + } + } } } return null; @@ -450,8 +462,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { return message; - } else { - return null; } } } @@ -504,10 +514,51 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return false; } + public List filterDuplicates(List list) { + HashMap items = new HashMap(); + for (Message item : list) { + items.put(item.getUuid(), item); + } + + ArrayList result = new ArrayList(items.values()); + Collections.sort(result, (o1, o2) -> { + if (o1.getTimeSent() < o2.getTimeSent()) + return -1; + if (o1.getTimeSent() > o2.getTimeSent()) + return 1; + return 0; + }); + return result; + } + public void populateWithMessages(final List messages) { synchronized (this.messages) { messages.clear(); - messages.addAll(this.messages); + messages.addAll(filterDuplicates(this.messages)); + + for (int n = 0; n < messages.size(); n++) { + if (messages.get(n).isMessageDeleted()) { + messages.remove(n); + n--; + continue; + } + + if (messages.get(n).getRetractId() != null) { + if (messages.get(n).getStatus() != Message.STATUS_RECEIVED) { + messages.remove(n); + n--; + continue; + } + } + } + + for (Message itm : messages) { + if (itm.isMessageDeleted()) { + if (itm.getEditedList().size() > 0) { + itm.setTime(itm.getEditedList().get(0).getTimeSent()); + } + } + } } for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) { if (iterator.next().wasMergedIntoPrevious()) { @@ -530,13 +581,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Jid getBlockedJid() { return getContact().getBlockedJid(); } - public String getLastReceivedOtrMessageId() { - return this.mLastReceivedOtrMessageId; - } - - public void setLastReceivedOtrMessageId(String id) { - this.mLastReceivedOtrMessageId = id; - } public int countMessages() { synchronized (this.messages) { @@ -734,112 +778,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public void setMode(int mode) { this.mode = mode; } - public SessionImpl startOtrSession(String presence, boolean sendStart) { - if (this.otrSession != null) { - return this.otrSession; - } else { - final SessionID sessionId = new SessionID(this.getJid().asBareJid().toString(), - presence, - "xmpp"); - this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService()); - try { - if (sendStart) { - this.otrSession.startSession(); - return this.otrSession; - } - return this.otrSession; - } catch (OtrException e) { - return null; - } - } - - } - - public SessionImpl getOtrSession() { - return this.otrSession; - } - - public void resetOtrSession() { - this.otrFingerprint = null; - this.otrSession = null; - this.mSmp.hint = null; - this.mSmp.secret = null; - this.mSmp.status = Smp.STATUS_NONE; - } - - public Smp smp() { - return mSmp; - } - - public boolean startOtrIfNeeded() { - if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { - try { - this.otrSession.startSession(); - return true; - } catch (OtrException e) { - this.resetOtrSession(); - return false; - } - } else { - return true; - } - } - - public boolean endOtrIfNeeded() { - if (this.otrSession != null) { - if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { - try { - this.otrSession.endSession(); - this.resetOtrSession(); - return true; - } catch (OtrException e) { - this.resetOtrSession(); - return false; - } - } else { - this.resetOtrSession(); - return false; - } - } else { - return false; - } - } - - public boolean hasValidOtrSession() { - return this.otrSession != null; - } - - public synchronized String getOtrFingerprint() { - if (this.otrFingerprint == null) { - try { - if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { - return null; - } - DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); - this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US); - } catch (final OtrCryptoException ignored) { - return null; - } catch (final UnsupportedOperationException ignored) { - return null; - } - } - return this.otrFingerprint; - } - - public boolean verifyOtrFingerprint() { - final String fingerprint = getOtrFingerprint(); - if (fingerprint != null) { - getContact().addOtrFingerprint(fingerprint); - return true; - } else { - return false; - } - } - - public boolean isOtrFingerprintVerified() { - return getContact().getOtrFingerprints().contains(getOtrFingerprint()); - } - /** * short for is Private and Non-anonymous @@ -876,7 +814,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public int getNextEncryption() { - if (!Config.supportOmemo() && !Config.supportOpenPgp() && !Config.supportOtr()) { + if (!Config.supportOmemo() && !Config.supportOpenPgp()) { return Message.ENCRYPTION_NONE; } if (OmemoSetting.isAlways()) { @@ -892,10 +830,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl defaultEncryption = Message.ENCRYPTION_NONE; } int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption); - if (encryption < 0) { + if (encryption == Message.ENCRYPTION_OTR || encryption < 0) { return defaultEncryption; - } else if (encryption == Message.ENCRYPTION_OTR) { - return encryption; } else { return encryption; } @@ -909,9 +845,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE); return nextMessage == null ? "" : nextMessage; } - public boolean smpRequested() { - return smp().status == Smp.STATUS_CONTACT_REQUESTED; - } public @Nullable Draft getDraft() { @@ -934,24 +867,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } return changed; } - public void setSymmetricKey(byte[] key) { - this.symmetricKey = key; - } - - public byte[] getSymmetricKey() { - return this.symmetricKey; - } public Bookmark getBookmark() { return this.account.getBookmark(this.contactJid); } - public Message findDuplicateMessage(Message message) { + public Message findDuplicateMessage(Message message, boolean withremoteid) { synchronized (this.messages) { for (int i = this.messages.size() - 1; i >= 0; --i) { if (this.messages.get(i).similar(message)) { return this.messages.get(i); } + if (withremoteid) { + if (this.messages.get(i).remoteMsgId != null && message.getRemoteMsgId() != null && this.messages.get(i).remoteMsgId.equals(message.getRemoteMsgId())) { + return this.messages.get(i); + } + } } } return null; @@ -1308,15 +1239,23 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return message; } } - public class Smp { - public static final int STATUS_NONE = 0; - public static final int STATUS_CONTACT_REQUESTED = 1; - public static final int STATUS_WE_REQUESTED = 2; - public static final int STATUS_FAILED = 3; - public static final int STATUS_VERIFIED = 4; - public String secret = null; - public String hint = null; - public int status = 0; + public Message findDuplicateMessage(Message message) { + return findDuplicateMessage(message, false); } -} \ No newline at end of file + + public boolean hasDuplicateMessage(Message message, boolean withremoteid) { + return findDuplicateMessage(message, withremoteid) != null; + } + + public Message findMessageWithUuidOrRemoteId(final String id) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (message.getRemoteMsgId() != null && message.getRemoteMsgId().equals(id) || message.getUuid().equals(id)) { + return message; + } + } + } + return null; + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Edit.java b/src/main/java/eu/siacs/conversations/entities/Edit.java index 79ef1fbaa..324b983de 100644 --- a/src/main/java/eu/siacs/conversations/entities/Edit.java +++ b/src/main/java/eu/siacs/conversations/entities/Edit.java @@ -11,16 +11,20 @@ public class Edit { private final String editedId; private final String serverMsgId; + private String body; + private final long timeSent; - Edit(String editedId, String serverMsgId) { + Edit(String editedId, String serverMsgId, String body, long timeSent) { this.editedId = editedId; this.serverMsgId = serverMsgId; + this.body = body; + this.timeSent = timeSent; } - static String toJson(List edits) throws JSONException { + static String toJson(List edits, boolean hidebody) throws JSONException { JSONArray jsonArray = new JSONArray(); for (Edit edit : edits) { - jsonArray.put(edit.toJson()); + jsonArray.put(edit.toJson(hidebody)); } return jsonArray.toString(); } @@ -46,7 +50,9 @@ public class Edit { private static Edit fromJson(JSONObject jsonObject) throws JSONException { String edited = jsonObject.has("edited_id") ? jsonObject.getString("edited_id") : null; String serverMsgId = jsonObject.has("server_msg_id") ? jsonObject.getString("server_msg_id") : null; - return new Edit(edited, serverMsgId); + String body = jsonObject.has("body") ? jsonObject.getString("body") : null; + long timeSent = jsonObject.has("timeSent") ? jsonObject.getLong("timeSent") : null; + return new Edit(edited, serverMsgId, body, timeSent); } static List fromJson(String input) { @@ -65,17 +71,15 @@ public class Edit { } } - private JSONObject toJson() throws JSONException { + private JSONObject toJson(boolean hidebody) throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("edited_id", editedId); jsonObject.put("server_msg_id", serverMsgId); + jsonObject.put("body", hidebody ? "" : body); + jsonObject.put("timeSent", timeSent); return jsonObject; } - String getEditedId() { - return editedId; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -94,4 +98,18 @@ public class Edit { result = 31 * result + (serverMsgId != null ? serverMsgId.hashCode() : 0); return result; } + + public String getServerMsgId() { + return serverMsgId; + } + + public String getBody() { + return body; + } + + public String getEditedId() { return editedId; } + + public long getTimeSent() { + return timeSent; + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java index 10bcf003f..a8ecb35a3 100644 --- a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java +++ b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java @@ -41,8 +41,8 @@ public class IndividualMessage extends Message { super(conversation); } - private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, boolean deleted, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable, boolean file_deleted, String bodyLanguage) { - super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, deleted, edited, oob, errorMessage, readByMarkers, markable, file_deleted, bodyLanguage); + private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, boolean deleted, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable, boolean file_deleted, String bodyLanguage, String retractId) { + super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, deleted, edited, oob, errorMessage, readByMarkers, markable, file_deleted, bodyLanguage,retractId); } public static Message createDateSeparator(Message message) { @@ -101,7 +101,8 @@ public class IndividualMessage extends Message { ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) + cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)), + cursor.getString(cursor.getColumnIndex(RETRACT_ID)) ); } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index f2c10b56c..0eb79f264 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -64,7 +64,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static final int TYPE_STATUS = 3; public static final int TYPE_PRIVATE = 4; public static final int TYPE_PRIVATE_FILE = 5; - public static final int TYPE_RTP_SESSION = 6; + public static final int TYPE_RTP_SESSION = 6; public static final String CONVERSATION = "conversationUuid"; public static final String COUNTERPART = "counterpart"; @@ -91,7 +91,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static final String ME_COMMAND = "/me"; public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; public static final String DELETED_MESSAGE_BODY = "eu.siacs.conversations.message_deleted"; - public static final String DELETED_MESSAGE_BODY_OLD = "de.monocles.chat.message_deleted"; + public static final String DELETED_MESSAGE_BODY_OLD = "de.pixart.messenger.message_deleted"; + public static final String RETRACT_ID = "retractId"; public boolean markable = false; protected String conversationUuid; @@ -111,6 +112,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable protected boolean read = true; protected boolean deleted = false; protected String remoteMsgId = null; + private String bodyLanguage = null; protected String serverMsgId = null; private final Conversational conversation; @@ -120,6 +122,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable private String axolotlFingerprint = null; private String errorMessage = null; private Set readByMarkers = new CopyOnWriteArraySet<>(); + private String retractId = null; private Boolean isGeoUri = null; private Boolean isXmppUri = null; @@ -162,21 +165,22 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable null, false, false, + null, null); } - public Message(Conversation conversation, int status, int type, final String remoteMsgId) { - this(conversation, java.util.UUID.randomUUID().toString(), - conversation.getUuid(), - conversation.getJid() == null ? null : conversation.getJid().asBareJid(), - null, - null, - System.currentTimeMillis(), - Message.ENCRYPTION_NONE, - status, - type, - false, - remoteMsgId, + public Message(Conversation conversation, int status, int type, final String remoteMsgId) { + this(conversation, java.util.UUID.randomUUID().toString(), + conversation.getUuid(), + conversation.getJid() == null ? null : conversation.getJid().asBareJid(), + null, + null, + System.currentTimeMillis(), + Message.ENCRYPTION_NONE, + status, + type, + false, + remoteMsgId, null, null, null, @@ -188,6 +192,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable null, false, false, + null, null); } @@ -197,7 +202,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable final String remoteMsgId, final String relativeFilePath, final String serverMsgId, final String fingerprint, final boolean read, final boolean deleted, final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, - final boolean markable, final boolean file_deleted, final String bodyLanguage) { + final boolean markable, final boolean file_deleted, final String bodyLanguage, final String retractId) { this.conversation = conversation; this.uuid = uuid; this.conversationUuid = conversationUUid; @@ -222,6 +227,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.markable = markable; this.file_deleted = file_deleted; this.bodyLanguage = bodyLanguage; + this.retractId = retractId; } public static Message fromCursor(Cursor cursor, Conversation conversation) { @@ -248,7 +254,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) + cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)), + cursor.getString(cursor.getColumnIndex(RETRACT_ID)) ); } @@ -306,7 +313,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable values.put(READ, read ? 1 : 0); values.put(DELETED, deleted ? 1 : 0); try { - values.put(EDITED, Edit.toJson(edits)); + values.put(EDITED, Edit.toJson(edits, retractId != null || deleted)); } catch (JSONException e) { Log.e(Config.LOGTAG, "error persisting json for edits", e); } @@ -316,6 +323,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable values.put(MARKABLE, markable ? 1 : 0); values.put(FILE_DELETED, file_deleted ? 1 : 0); values.put(BODY_LANGUAGE, bodyLanguage); + values.put(RETRACT_ID, retractId); return values; } @@ -486,8 +494,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.carbon = carbon; } - public void putEdited(String edited, String serverMsgId) { - final Edit edit = new Edit(edited, serverMsgId); + public void putEdited(String edited, String serverMsgId, String body, long timeSent) { + final Edit edit = new Edit(edited, serverMsgId, body, timeSent); if (this.edits.size() < 128 && !this.edits.contains(edit)) { this.edits.add(edit); } @@ -526,6 +534,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return this.transferable; } + public String getRetractId() { + return this.retractId; + } + + public void setRetractId(String id) { + this.retractId = id; + } + public synchronized void setTransferable(Transferable transferable) { this.fileParams = null; this.transferable = transferable; @@ -638,54 +654,62 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public boolean isLastCorrectableMessage() { Message next = next(); while (next != null) { - if (next.isEditable()) { + if (next.isEditable()) { return false; } next = next.next(); } - return isEditable(); + return isEditable(); } public boolean isEditable() { - return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION; + return status != STATUS_RECEIVED && type != Message.TYPE_RTP_SESSION; } public boolean mergeable(final Message message) { - return message != null && - (message.getType() == Message.TYPE_TEXT && - this.getTransferable() == null && - message.getTransferable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && - this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && - isStatusMergeable(this.getStatus(), message.getStatus()) && - this.getEncryption() == message.getEncryption() && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - this.edited() == message.edited() && - !this.isMessageDeleted() == !message.isMessageDeleted() && - (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && - this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS && - !message.isGeoUri() && - !this.isGeoUri() && - !message.isWebUri() && - !this.isWebUri() && - !message.isOOb() && - !this.isOOb() && - !message.treatAsDownloadable() && - !this.treatAsDownloadable() && - !message.hasMeCommand() && - !this.hasMeCommand() && - !message.bodyIsOnlyEmojis() && - !this.bodyIsOnlyEmojis() && - !message.isXmppUri() && - !this.isXmppUri() && - ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && - UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && - this.getReadByMarkers().equals(message.getReadByMarkers()) && - !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) - ); + try { + boolean mergeAllowed = conversation.getAccount().getXmppConnection().getXmppConnectionService().allowMergeMessages(); + return mergeAllowed && message != null && + (message.getType() == Message.TYPE_TEXT && + this.getTransferable() == null && + message.getTransferable() == null && + message.getEncryption() != Message.ENCRYPTION_PGP && + message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && + this.getType() == message.getType() && + //this.getStatus() == message.getStatus() && + isStatusMergeable(this.getStatus(), message.getStatus()) && + this.getEncryption() == message.getEncryption() && + this.getCounterpart() != null && + this.getCounterpart().equals(message.getCounterpart()) && + this.edited() == message.edited() && + !this.isMessageDeleted() == !message.isMessageDeleted() && + (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && + this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS && + !message.isGeoUri() && + !this.isGeoUri() && + !message.isWebUri() && + !this.isWebUri() && + !message.isOOb() && + !this.isOOb() && + !message.treatAsDownloadable() && + !this.treatAsDownloadable() && + !message.hasMeCommand() && + !this.hasMeCommand() && + !message.bodyIsOnlyEmojis() && + !this.bodyIsOnlyEmojis() && + !message.isXmppUri() && + !this.isXmppUri() && + !message.hasDeletedBody() && + !this.hasDeletedBody() && + ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && + UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && + this.getReadByMarkers().equals(message.getReadByMarkers()) && + !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) + ); + } catch (Exception e) { + e.printStackTrace(); + } + return false; } private static boolean isStatusMergeable(int a, int b) { @@ -746,6 +770,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return this.body.trim().startsWith(ME_COMMAND); } + public boolean hasDeletedBody() { + return this.body.trim().equals(DELETED_MESSAGE_BODY) || this.body.trim().equals(DELETED_MESSAGE_BODY_OLD); + } + public int getMergedStatus() { int status = this.status; Message current = this; @@ -783,8 +811,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public boolean fixCounterpart() { - final Presences presences = conversation.getContact().getPresences(); - if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { + final Presences presences = conversation.getContact().getPresences(); + if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { return true; } else if (presences.size() >= 1) { counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]); @@ -807,6 +835,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } } + public List getEditedList() { + return edits; + } + public String getEditedIdWireFormat() { if (edits.size() > 0) { return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); @@ -824,16 +856,16 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable if (relativeFilePath != null) { extension = MimeUtils.extractRelevantExtension(relativeFilePath); } else { - try { - final String url = URL.tryParse(body.split("\n")[0]); - if (url == null) { - return null; - } - extension = MimeUtils.extractRelevantExtension(url); - } catch (Exception e) { - return null; - } - } + try { + final String url = URL.tryParse(body.split("\n")[0]); + if (url == null) { + return null; + } + extension = MimeUtils.extractRelevantExtension(url); + } catch (Exception e) { + return null; + } + } return MimeUtils.guessMimeTypeFromExtension(extension); } @@ -977,6 +1009,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return isFileOrImage() && getFileParams().url == null; } + public boolean fileIsTransferring() { + return transferable.getStatus() == Transferable.STATUS_DOWNLOADING || transferable.getStatus() == Transferable.STATUS_UPLOADING || transferable.getStatus() == Transferable.STATUS_WAITING; + } + public static class FileParams { public String url; public Long size = null; @@ -984,7 +1020,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public int height = 0; public int runtime = 0; public String subject = ""; - public long getSize() { return size == null ? 0 : size; } @@ -1066,13 +1101,28 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } if (conversation.getMode() == Conversation.MODE_MULTI) { final Jid nextCounterpart = conversation.getNextCounterpart(); - if (nextCounterpart != null) { - message.setCounterpart(nextCounterpart); - message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart)); - message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); - return true; - } + return configurePrivateMessage(conversation, message, nextCounterpart, isFile); } return false; } -} \ No newline at end of file + + public static boolean configurePrivateMessage(final Message message, final Jid counterpart) { + final Conversation conversation; + if (message.conversation instanceof Conversation) { + conversation = (Conversation) message.conversation; + } else { + return false; + } + return configurePrivateMessage(conversation, message, counterpart, false); + } + + private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) { + if (counterpart == null) { + return false; + } + message.setCounterpart(counterpart); + message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart)); + message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); + return true; + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 80ac14a74..1f4831b90 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.entities; -import android.annotation.SuppressLint; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -25,7 +24,6 @@ import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.pep.Avatar; -@SuppressLint("DefaultLocale") public class MucOptions { public static final String STATUS_CODE_SELF_PRESENCE = "110"; @@ -213,6 +211,10 @@ public class MucOptions { return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false); } + public boolean stableId() { + return getFeatures().contains("http://jabber.org/protocol/muc#stable_id"); + } + public User deleteUser(Jid jid) { User user = findUserByFullJid(jid); if (user != null) { @@ -630,8 +632,8 @@ public class MucOptions { OUTCAST(0, R.string.outcast), NONE(1, R.string.no_affiliation); - private int resId; - private int rank; + private final int resId; + private final int rank; Affiliation(int rank, int resId) { this.resId = resId; @@ -673,8 +675,8 @@ public class MucOptions { PARTICIPANT(R.string.participant, 2), NONE(R.string.no_role, 0); - private int resId; - private int rank; + private final int resId; + private final int rank; Role(int resId, int rank) { this.resId = resId; @@ -741,7 +743,7 @@ public class MucOptions { private Jid fullJid; private long pgpKeyId = 0; private Avatar avatar; - private MucOptions options; + private final MucOptions options; private ChatState chatState = Config.DEFAULT_CHAT_STATE; public User(MucOptions options, Jid fullJid) { @@ -852,7 +854,7 @@ public class MucOptions { @Override public String toString() { - return "[fulljid:" + String.valueOf(fullJid) + ",realjid:" + String.valueOf(realJid) + ",affiliation" + affiliation.toString() + "]"; + return "[fulljid:" + fullJid + ",realjid:" + realJid + ",affiliation" + affiliation.toString() + "]"; } public boolean realJidMatchesAccount() { diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index ad171a88e..b94470db4 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -223,9 +223,9 @@ public class ServiceDiscoveryResult { for (Data form : forms) { s.append(clean(form.getFormType())).append("<"); List fields = form.getFields(); - Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName()))); + Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName()))); for (Field field : fields) { - s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); + s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); List values = field.getValues(); Collections.sort(values); for (String value : values) { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index bfcc700b7..507c3550d 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -12,7 +12,6 @@ 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; @@ -54,12 +53,12 @@ public abstract class AbstractGenerator { private final String[] MESSAGE_CORRECTION_FEATURES = { "urn:xmpp:message-correct:0" }; + private final String[] MESSAGE_RETRACTION_FEATURES = { + "urn:xmpp:message-retract:0" + }; private final String[] PRIVACY_SENSITIVE = { "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone }; - private final String[] OTR = { - "urn:xmpp:otr:0" - }; private final String[] VOIP_NAMESPACES = { Namespace.JINGLE_TRANSPORT_ICE_UDP, Namespace.JINGLE_FEATURE_AUDIO, @@ -70,6 +69,7 @@ public abstract class AbstractGenerator { }; protected XmppConnectionService mXmppConnectionService; + private String mVersion = null; AbstractGenerator(XmppConnectionService service) { this.mXmppConnectionService = service; @@ -81,11 +81,18 @@ public abstract class AbstractGenerator { } String getIdentityVersion() { - return BuildConfig.VERSION_NAME; + if (mVersion == null) { + this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); + } + return this.mVersion; } public String getIdentityName() { - return BuildConfig.APP_NAME; + return mXmppConnectionService.getString(R.string.app_name) + ' ' + getIdentityVersion(); + } + + public String getUserAgent() { + return System.getProperty("http.agent"); } String getIdentityType() { @@ -122,6 +129,9 @@ public abstract class AbstractGenerator { if (mXmppConnectionService.allowMessageCorrection()) { features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES)); } + if (mXmppConnectionService.allowMessageRetraction()) { + features.addAll(Arrays.asList(MESSAGE_RETRACTION_FEATURES)); + } if (Config.supportOmemo()) { features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY); } @@ -129,9 +139,6 @@ public abstract class AbstractGenerator { features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); features.addAll(Arrays.asList(VOIP_NAMESPACES)); } - if (Config.supportOtr()) { - features.addAll(Arrays.asList(OTR)); - } if (mXmppConnectionService.broadcastLastActivity()) { features.add(Namespace.IDLE); } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 0f0771b0c..2389f45e2 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -22,11 +22,8 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; public class MessageGenerator extends AbstractGenerator { - public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that"; private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that."; @@ -61,9 +58,17 @@ public class MessageGenerator extends AbstractGenerator { } packet.setFrom(account.getJid()); packet.setId(message.getUuid()); - packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid()); - if (message.edited()) { + if (conversation.getMode() == Conversational.MODE_SINGLE || message.isPrivateMessage() || !conversation.getMucOptions().stableId()) { + packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid()); + } + if (message.edited() && !message.isMessageDeleted()) { packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat()); + } else if (message.isMessageDeleted()) { + Element apply = packet.addChild("apply-to", "urn:xmpp:fasten:0").setAttribute("id", (message.getRetractId() != null ? message.getRetractId() : (message.getRemoteMsgId() != null ? message.getRemoteMsgId() : (message.getEditedIdWireFormat() != null ? message.getEditedIdWireFormat() : message.getUuid())))); + apply.addChild("retract", "urn:xmpp:message-retract:0"); + packet.addChild("fallback", "urn:xmpp:fallback:0"); + packet.addChild("store", "urn:xmpp:hints"); + packet.setBody("This person attempted to retract a previous message, but it's unsupported by your client."); } return packet; } @@ -107,29 +112,6 @@ public class MessageGenerator extends AbstractGenerator { packet.addChild("no-permanent-store", "urn:xmpp:hints"); packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store* } - public MessagePacket generateOtrChat(Message message) { - Conversation conversation = (Conversation) message.getConversation(); - Session otrSession = conversation.getOtrSession(); - if (otrSession == null) { - return null; - } - MessagePacket packet = preparePacket(message); - addMessageHints(packet); - try { - String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); - } else { - content = message.getBody(); - } - packet.setBody(otrSession.transformSending(content)[0]); - packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0"); - return packet; - } catch (OtrException e) { - return null; - } - } - public MessagePacket generateChat(Message message) { MessagePacket packet = preparePacket(message); @@ -141,7 +123,8 @@ public class MessageGenerator extends AbstractGenerator { } else { content = message.getBody(); } - packet.setBody(content); + if (!message.isMessageDeleted()) + packet.setBody(content); return packet; } @@ -274,7 +257,7 @@ public class MessageGenerator extends AbstractGenerator { error.addChild("text").setContent("?OTR Error:" + errorText); return packet; } - + public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { final MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 631edf4b1..a839d050b 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -77,7 +77,7 @@ public class PresenceGenerator extends AbstractGenerator { Element cap = packet.addChild("c", "http://jabber.org/protocol/caps"); cap.setAttribute("hash", "sha-1"); - cap.setAttribute("node", "http://monocles.de"); + cap.setAttribute("node", "http://blabber.im"); cap.setAttribute("ver", capHash); } return packet; diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index d6d151d12..c222acbd0 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -22,7 +22,6 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; @@ -55,10 +54,10 @@ public class HttpConnectionManager extends AbstractConnectionManager { .build(); } - public static String getUserAgent() { - return String.format("%s/%s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME); + return System.getProperty("http.agent"); } + public HttpConnectionManager(XmppConnectionService service) { super(service); } @@ -149,7 +148,8 @@ public class HttpConnectionManager extends AbstractConnectionManager { final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); builder.sslSocketFactory(sf, trustManager); builder.hostnameVerifier(new StrictHostnameVerifier()); - } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { + } catch (final KeyManagementException ignored) { + } catch (final NoSuchAlgorithmException ignored) { } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index ea97e990f..2045491e0 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.http; +import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor; + import android.util.Log; import androidx.annotation.Nullable; @@ -35,8 +37,6 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor; - public class HttpDownloadConnection implements Transferable { private final Message message; @@ -114,13 +114,12 @@ public class HttpDownloadConnection implements Transferable { } } setupFile(); - if ((this.message.getEncryption() == Message.ENCRYPTION_OTR - || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL) - && this.file.getKey() == null) { + if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } //TODO add auth tag size to knownFileSize final Long knownFileSize = message.getFileParams().size; + Log.d(Config.LOGTAG, "knownFileSize: " + knownFileSize + ", body=" + message.getBody()); if (knownFileSize != null && interactive) { this.file.setExpectedSize(knownFileSize); download(true); @@ -145,7 +144,7 @@ public class HttpDownloadConnection implements Transferable { private void download(final boolean interactive) { changeStatus(STATUS_WAITING); - Log.d(Config.LOGTAG,"download()",new Exception()); + Log.d(Config.LOGTAG, "download()", new Exception()); FileTransferExecutor.execute(new FileDownloader(interactive)); } diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index 7d7d7d3ee..85179d880 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.http; +import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor; + import android.util.Log; import com.google.common.util.concurrent.FutureCallback; @@ -30,8 +32,6 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor; - public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener { static final List WHITE_LISTED_HEADERS = Arrays.asList( @@ -185,26 +185,26 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan @Override public void onResponse(@NotNull Call call, @NotNull Response response) { final int code = response.code(); - if (code == 200 || code == 201) { - Log.d(Config.LOGTAG, "finished uploading file"); - final String get; - if (key != null) { + if (code == 200 || code == 201) { + Log.d(Config.LOGTAG, "finished uploading file"); + final String get; + if (key != null) { get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build()); } else { get = slot.get.toString(); + } + mXmppConnectionService.getFileBackend().updateFileParams(message, get); + mXmppConnectionService.getFileBackend().updateMediaScanner(file); + finish(); + if (!message.isPrivateMessage()) { + message.setCounterpart(message.getConversation().getJid().asBareJid()); + } + mXmppConnectionService.resendMessage(message, delayed); + } else { + Log.d(Config.LOGTAG, "http upload failed because response code was " + code); + fail("http upload failed because response code was " + code); } - mXmppConnectionService.getFileBackend().updateFileParams(message, get); - mXmppConnectionService.getFileBackend().updateMediaScanner(file); - finish(); - if (!message.isPrivateMessage()) { - message.setCounterpart(message.getConversation().getJid().asBareJid()); - } - mXmppConnectionService.resendMessage(message, delayed); - } else { - Log.d(Config.LOGTAG, "http upload failed because response code was " + code); - fail("http upload failed because response code was " + code); } - } }); } diff --git a/src/main/java/eu/siacs/conversations/http/Method.java b/src/main/java/eu/siacs/conversations/http/Method.java index 526ae38a3..4ddb8df74 100644 --- a/src/main/java/eu/siacs/conversations/http/Method.java +++ b/src/main/java/eu/siacs/conversations/http/Method.java @@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xmpp.XmppConnection; public enum Method { - HTTP_UPLOAD, HTTP_UPLOAD_LEGACY; + HTTP_UPLOAD, HTTP_UPLOAD_LEGACY; public static Method determine(Account account) { XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures(); diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java index d6ee6a28b..149027704 100644 --- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java +++ b/src/main/java/eu/siacs/conversations/http/SlotRequester.java @@ -114,7 +114,7 @@ public class SlotRequester { final String name = child.getAttribute("name"); final String value = child.getContent(); if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) { - headers.put(name, value.trim()); + headers.put(name, value.trim()); } } } diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 68112169a..3320e3cd7 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -139,19 +139,19 @@ public abstract class AbstractParser { return null; } - public static String errorMessage(Element packet) { - final Element error = packet.findChild("error"); - if (error != null && error.getChildren().size() > 0) { - final List errorNames = orderedElementNames(error.getChildren()); - final String text = error.findChildContent("text"); - if (text != null && !text.trim().isEmpty()) { - return text; - } else if (errorNames.size() > 0){ - return errorNames.get(0).replace("-"," "); - } - } - return null; - } + public static String errorMessage(Element packet) { + final Element error = packet.findChild("error"); + if (error != null && error.getChildren().size() > 0) { + final List errorNames = orderedElementNames(error.getChildren()); + final String text = error.findChildContent("text"); + if (text != null && !text.trim().isEmpty()) { + return text; + } else if (errorNames.size() > 0){ + return errorNames.get(0).replace("-"," "); + } + } + return null; + } private static String prefixError(List errorNames) { if (errorNames.size() > 0) { diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 3f11c3fcc..a73e35de9 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -463,5 +463,5 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - ; + ; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index e3e0122e9..db2222f25 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -1,8 +1,5 @@ package eu.siacs.conversations.parser; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; - import android.util.Log; import android.util.Pair; @@ -16,14 +13,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; -import android.os.Build; -import android.text.Html; -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionStatus; -import eu.siacs.conversations.crypto.OtrService; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -37,6 +26,7 @@ import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.Edit; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.ReadByMarker; @@ -58,15 +48,12 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; + public class MessageParser extends AbstractParser implements OnMessagePacketReceived { - private static final List CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian"); private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); - private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract"); public MessageParser(XmppConnectionService service) { @@ -108,29 +95,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); return result != null ? result : fallback; } - private static boolean clientMightSendHtml(Account account, Jid from) { - String resource = from.getResource(); - if (resource == null) { - return false; - } - Presence presence = account.getRoster().getContact(from).getPresences().getPresencesMap().get(resource); - ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult(); - if (disco == null) { - return false; - } - return hasIdentityKnowForSendingHtml(disco.getIdentities()); - } - - private static boolean hasIdentityKnowForSendingHtml(List identities) { - for (ServiceDiscoveryResult.Identity identity : identities) { - if (identity.getName() != null) { - if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) { - return true; - } - } - } - return false; - } private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) { ChatState state = ChatState.parse(packet); @@ -163,66 +127,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return false; } - private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) { - String presence; - if (from.isBareJid()) { - presence = ""; - } else { - presence = from.getResource(); - } - if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { - conversation.endOtrIfNeeded(); - } - if (!conversation.hasValidOtrSession()) { - conversation.startOtrSession(presence, false); - } else { - String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); - if (!foreignPresence.equals(presence)) { - conversation.endOtrIfNeeded(); - conversation.startOtrSession(presence, false); - } - } - try { - conversation.setLastReceivedOtrMessageId(id); - Session otrSession = conversation.getOtrSession(); - body = otrSession.transformReceiving(body); - SessionStatus status = otrSession.getSessionStatus(); - if (body == null && status == SessionStatus.ENCRYPTED) { - mXmppConnectionService.onOtrSessionEstablished(conversation); - return null; - } else if (body == null && status == SessionStatus.FINISHED) { - conversation.resetOtrSession(); - mXmppConnectionService.updateConversationUi(); - return null; - } else if (body == null || (body.isEmpty())) { - return null; - } - if (body.startsWith(CryptoHelper.FILETRANSFER)) { - String key = body.substring(CryptoHelper.FILETRANSFER.length()); - conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); - return null; - } - if (clientMightSendHtml(conversation.getAccount(), from)) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OTR message from bad behaving client. escaping HTML…"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString(); - } else { - body = Html.fromHtml(body).toString(); - } - } - - final OtrService otrService = conversation.getAccount().getOtrService(); - Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED); - finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey())); - conversation.setLastReceivedOtrMessageId(null); - - return finishedMessage; - } catch (Exception e) { - conversation.resetOtrSession(); - return null; - } - } - private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) { final AxolotlService service = conversation.getAccount().getAxolotlService(); final XmppAxolotlMessage xmppAxolotlMessage; @@ -446,14 +350,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } } - - if (message != null) { - if (message.getEncryption() == Message.ENCRYPTION_OTR) { - Conversation conversation = (Conversation) message.getConversation(); - conversation.endOtrIfNeeded(); - } - } - } return true; } @@ -520,6 +416,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Element oob = packet.findChild("x", Namespace.OOB); final String oobUrl = oob != null ? oob.findChildContent("url") : null; final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); + + final Element applyToElement = packet.findChild("apply-to", "urn:xmpp:fasten:0"); + final String retractId = applyToElement != null && applyToElement.findChild("retract", "urn:xmpp:message-retract:0") != null ? applyToElement.getAttribute("id") : null; + final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); int status; final Jid counterpart; @@ -609,20 +509,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } final Message message; - if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) { - if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) { - message = parseOtrChat(body.content, from, remoteMsgId, conversation); - if (message == null) { - return; - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed)); - message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status); - if (body.count > 1) { - message.setBodyLanguage(body.language); - } - } - } else if (pgpEncrypted != null && Config.supportOpenPgp()) { + if (pgpEncrypted != null && Config.supportOpenPgp()) { message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); } else if (axolotlEncrypted != null && Config.supportOmemo()) { Jid origin; @@ -733,10 +620,14 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) { - final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, + Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, counterpart, message.getStatus() == Message.STATUS_RECEIVED, message.isCarbon()); + + if (replacedMessage == null) { + replacedMessage = conversation.findSentMessageWithUuidOrRemoteId(replacementId, true, true); + } if (replacedMessage != null) { final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null || replacedMessage.getFingerprint().equals(message.getFingerprint()); @@ -749,10 +640,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'"); synchronized (replacedMessage) { final String uuid = replacedMessage.getUuid(); + + replacedMessage.putEdited(replacedMessage.getRemoteMsgId() != null ? replacedMessage.getRemoteMsgId() : replacedMessage.getUuid(), replacedMessage.getServerMsgId(), replacedMessage.getBody(), replacedMessage.getTimeSent()); + replacedMessage.setUuid(UUID.randomUUID().toString()); replacedMessage.setBody(message.getBody()); - replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId()); replacedMessage.setRemoteMsgId(remoteMsgId); + replacedMessage.setTime(message.getTimeSent()); if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) { replacedMessage.setServerMsgId(message.getServerMsgId()); } @@ -781,11 +675,80 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out"); } } - } else if (replacementId != null && !mXmppConnectionService.allowMessageCorrection() && (message.getBody().equals(DELETED_MESSAGE_BODY) || message.getBody().equals(DELETED_MESSAGE_BODY_OLD))) { + } else if (replacementId != null && !mXmppConnectionService.allowMessageCorrection() && (message.hasDeletedBody())) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received deleted message but LMC is deactivated"); return; } + if (retractId != null && mXmppConnectionService.allowMessageRetraction()) { + final Message retractedMessage = conversation.findSentMessageWithUuidOrRemoteId(retractId, true, true); + if (retractedMessage != null) { + final boolean fingerprintsMatch = retractedMessage.getFingerprint() == null + || retractedMessage.getFingerprint().equals(message.getFingerprint()); + final boolean trueCountersMatch = retractedMessage.getTrueCounterpart() != null + && message.getTrueCounterpart() != null + && retractedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid()); + final boolean mucUserMatches = query == null && retractedMessage.sameMucUser(message); //can not be checked when using mam + final boolean duplicate = conversation.hasDuplicateMessage(message); + List lAcc = mXmppConnectionService.getAccounts(); + boolean activeSelf = false; + if (message.getTrueCounterpart()!=null) { + for (Account a : lAcc) { + if (a.getJid() != null && a.isOnlineAndConnected() && a.getJid().asBareJid().equals(message.getTrueCounterpart().asBareJid())) { + activeSelf = true; + break; + } + } + } + if (fingerprintsMatch && ((trueCountersMatch || !conversationMultiMode || mucUserMatches || (isCarbon&&activeSelf) && !duplicate) || conversationMultiMode)) { + Log.d(Config.LOGTAG, "retracted message '" + retractedMessage.getBody() + "' with '" + message.getBody() + "'"); + synchronized (retractedMessage) { + + retractedMessage.setBody(mXmppConnectionService.getString(R.string.message_deleted)); + retractedMessage.setRetractId(retractId); + + extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet); + + message.setMessageDeleted(true); + message.setRetractId(retractId); + + if (message.getStatus() > Message.STATUS_RECEIVED) { + retractedMessage.setMessageDeleted(true); + } + + for (Edit itm : retractedMessage.getEditedList()) { + Message tmpRetractedMessage = conversation.findMessageWithUuidOrRemoteId(itm.getEditedId()); + if (tmpRetractedMessage != null) { + tmpRetractedMessage.setRetractId(retractId); + mXmppConnectionService.updateMessage(tmpRetractedMessage, tmpRetractedMessage.getUuid()); + } + } + mXmppConnectionService.updateMessage(retractedMessage, retractedMessage.getUuid()); + mXmppConnectionService.databaseBackend.createMessage(message); + if (mXmppConnectionService.confirmMessages() + && retractedMessage.getStatus() == Message.STATUS_RECEIVED + && (retractedMessage.trusted() || retractedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs? + && remoteMsgId != null + && !selfAddressed + && !isTypeGroupChat) { + processMessageReceipts(account, packet, remoteMsgId, query); + } + } + mXmppConnectionService.getNotificationService().updateNotification(); + return; + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message retraction but checks are not valid"); + } + } + else { + //we deleted a carbon from ourself and the dialog allready removed it from ui + message.setMessageDeleted(true); + message.setRetractId(retractId); + mXmppConnectionService.databaseBackend.createMessage(message); + return; + } + } + long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate(); if (deletionDate != 0 && message.getTimeSent() < deletionDate) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date"); @@ -859,12 +822,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece && !isTypeGroupChat) { processMessageReceipts(account, packet, remoteMsgId, query); } - if (message.getStatus() == Message.STATUS_RECEIVED - && conversation.getOtrSession() != null - && !conversation.getOtrSession().getSessionID().getUserID() - .equals(message.getCounterpart().getResource())) { - conversation.endOtrIfNeeded(); - } mXmppConnectionService.databaseBackend.createMessage(message); final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); @@ -1256,4 +1213,4 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return false; } } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 3ba4345cb..7bece26dc 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -313,10 +313,10 @@ public class PresenceParser extends AbstractParser implements PgpEngine pgp = mXmppConnectionService.getPgpEngine(); Element x = packet.findChild("x", "jabber:x:signed"); if (pgp != null && x != null) { - final String status = packet.findChildContent("status"); - final long keyId = pgp.fetchKeyId(account, status, x.getContent()); - if (keyId != 0 && contact.setPgpKeyId(keyId)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId)); + final String status = packet.findChildContent("status"); + final long keyId = pgp.fetchKeyId(account, status, x.getContent()); + if (keyId != 0 && contact.setPgpKeyId(keyId)) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId)); mXmppConnectionService.syncRoster(account); } } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 01b47c9bb..1c0b35101 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.persistance; +import static eu.siacs.conversations.ui.util.UpdateHelper.moveData_PAM_monocles; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -11,6 +13,8 @@ import android.os.SystemClock; import android.util.Base64; import android.util.Log; +import com.google.common.base.Stopwatch; + import org.json.JSONException; import org.json.JSONObject; import org.whispersystems.libsignal.IdentityKey; @@ -58,12 +62,11 @@ import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.mam.MamReference; -import static eu.siacs.conversations.ui.util.UpdateHelper.moveData_PAM_monocles; - public class DatabaseBackend extends SQLiteOpenHelper { public static final String DATABASE_NAME = "history"; - public static final int DATABASE_VERSION = 54; // = Conversations DATABASE_VERSION + 6 + public static final int DATABASE_VERSION = 56; // = Conversations DATABASE_VERSION + 7 + private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; private static final String CREATE_CONTATCS_STATEMENT = "create table " @@ -170,14 +173,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static String CREATE_MESSAGE_TIME_INDEX = "CREATE INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")"; private static String CREATE_MESSAGE_CONVERSATION_INDEX = "CREATE INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")"; private static String CREATE_MESSAGE_DELETED_INDEX = "CREATE INDEX message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")"; - private static String CREATE_MESSAGE_FILE_DELETED_INDEX = "CREATE INDEX message_file_deleted_index ON " + Message.TABLENAME + "(" + Message.FILE_DELETED + ")"; + private static String CREATE_MESSAGE_FILE_DELETED_INDEX = "create index message_file_deleted_index ON " + Message.TABLENAME + "(" + Message.FILE_DELETED + ")"; private static String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")"; private static String CREATE_MESSAGE_TYPE_INDEX = "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")"; - private static String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4(uuid TEXT PRIMARY KEY, body TEXT, tokenize = 'unicode61')"; - private static String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index (uuid,body) VALUES (new.uuid,new.body); END;"; - private static String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE of uuid,body ON " + Message.TABLENAME + " BEGIN update messages_index set body=new.body,uuid=new.uuid WHERE uuid=old.uuid; END;"; - private static String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(uuid,body) SELECT uuid,body FROM " + Message.TABLENAME + ";"; + private static String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')"; + private static String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index(rowid,uuid,body) VALUES(NEW.rowid,NEW.uuid,NEW.body); END;"; + private static String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON " + Message.TABLENAME + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE rowid=OLD.rowid; END;"; + private static final String CREATE_MESSAGE_DELETE_TRIGGER = "CREATE TRIGGER after_message_delete AFTER DELETE ON " + Message.TABLENAME + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;"; + private static String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');"; private DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -190,6 +194,17 @@ public class DatabaseBackend extends SQLiteOpenHelper { return values; } + public static boolean requiresMessageIndexRebuild() { + return requiresMessageIndexRebuild; + } + + public void rebuildMessagesIndex() { + final SQLiteDatabase db = getWritableDatabase(); + final Stopwatch stopwatch = Stopwatch.createStarted(); + db.execSQL(COPY_PREEXISTING_ENTRIES); + Log.d(Config.LOGTAG,"rebuilt message index in "+ stopwatch.stop().toString()); + } + public static synchronized DatabaseBackend getInstance(Context context) { if (instance == null) { instance = new DatabaseBackend(context); @@ -247,6 +262,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.MARKABLE + " NUMBER DEFAULT 0," + Message.FILE_DELETED + " NUMBER DEFAULT 0," + Message.BODY_LANGUAGE + " TEXT," + + Message.RETRACT_ID + " TEXT," + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -269,6 +285,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_MESSAGE_INDEX_TABLE); db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); + db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER); } @Override @@ -466,13 +483,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 36 && newVersion >= 36) { // only rename videos, images, audios and other files directories - final File oldPicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Images/"); - final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Files/"); - final File oldAudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Audios/"); - final File oldVideosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Videos/"); + final File oldPicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Images/"); + final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Files/"); + final File oldAudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Audios/"); + final File oldVideosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Videos/"); if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) { - final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Images/"); + final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Images/"); newPicturesDirectory.getParentFile().mkdirs(); final File[] files = oldPicturesDirectory.listFiles(); if (files == null) { @@ -483,7 +500,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) { - final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Files/"); + final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Files/"); newFilesDirectory.mkdirs(); final File[] files = oldFilesDirectory.listFiles(); if (files == null) { @@ -494,7 +511,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } if (oldAudiosDirectory.exists() && oldAudiosDirectory.isDirectory()) { - final File newAudiosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Audios/"); + final File newAudiosDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Audios/"); newAudiosDirectory.mkdirs(); final File[] files = oldAudiosDirectory.listFiles(); if (files == null) { @@ -505,7 +522,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } if (oldVideosDirectory.exists() && oldVideosDirectory.isDirectory()) { - final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Videos/"); + final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Videos/"); newVideosDirectory.mkdirs(); final File[] files = oldVideosDirectory.listFiles(); if (files == null) { @@ -535,13 +552,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0"); } - if (oldVersion < 42 && newVersion >= 42) { - db.execSQL(CREATE_MESSAGE_INDEX_TABLE); - db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); - db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); - db.execSQL(COPY_PREEXISTING_ENTRIES); - } - if (oldVersion < 43 && newVersion >= 43) { db.execSQL("DROP TRIGGER IF EXISTS after_message_delete"); } @@ -578,9 +588,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 51 && newVersion >= 51) { - // values in resolver_result are cache and not worth to store - db.execSQL("DROP TABLE IF EXISTS " + RESOLVER_RESULTS_TABLENAME); - db.execSQL(CREATE_RESOLVER_RESULTS_TABLE); + // values in resolver_result are cache and not worth to store + db.execSQL("DROP TABLE IF EXISTS " + RESOLVER_RESULTS_TABLENAME); + db.execSQL(CREATE_RESOLVER_RESULTS_TABLE); } if (oldVersion < 52 && newVersion >= 52) { @@ -594,6 +604,33 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 54 && newVersion >= 54) { db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT"); } + if (oldVersion < 55 && newVersion >= 55) { + db.beginTransaction(); + db.execSQL("DROP TRIGGER IF EXISTS after_message_insert;"); + db.execSQL("DROP TRIGGER IF EXISTS after_message_update;"); + db.execSQL("DROP TRIGGER IF EXISTS after_message_delete;"); + db.execSQL("DROP TABLE IF EXISTS messages_index;"); + // a hack that should not be necessary, but + // there was at least one occurence when SQLite failed at this + db.execSQL("DROP TABLE IF EXISTS messages_index_docsize;"); + db.execSQL("DROP TABLE IF EXISTS messages_index_segdir;"); + db.execSQL("DROP TABLE IF EXISTS messages_index_segments;"); + db.execSQL("DROP TABLE IF EXISTS messages_index_stat;"); + db.execSQL(CREATE_MESSAGE_INDEX_TABLE); + db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); + db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); + db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER); + db.setTransactionSuccessful(); + db.endTransaction(); + requiresMessageIndexRebuild = true; + } + if (oldVersion < 56 && newVersion >= 56) { + db.beginTransaction(); + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.RETRACT_ID + " TEXT;"); + db.setTransactionSuccessful(); + db.endTransaction(); + requiresMessageIndexRebuild = true; + } } private boolean isColumnExisting(SQLiteDatabase db, String TableName, String ColumnName) { @@ -832,12 +869,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { final SQLiteDatabase db = this.getReadableDatabase(); final StringBuilder SQL = new StringBuilder(); final String[] selectionArgs; - SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + '.' + Conversation.CONTACTJID + ',' + Conversation.TABLENAME + '.' + Conversation.ACCOUNT + ',' + Conversation.TABLENAME + '.' + Conversation.MODE + " FROM " + Message.TABLENAME + " join " + Conversation.TABLENAME + " on " + Message.TABLENAME + '.' + Message.CONVERSATION + '=' + Conversation.TABLENAME + '.' + Conversation.UUID + " join messages_index ON messages_index.uuid=messages.uuid where " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + ',' + Message.ENCRYPTION_PGP + ',' + Message.ENCRYPTION_DECRYPTION_FAILED + ',' + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + ',' + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?"); + SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + "," + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.MODE + " FROM " + Message.TABLENAME + " JOIN " + Conversation.TABLENAME + " ON " + Message.TABLENAME + "." + Message.CONVERSATION + "=" + Conversation.TABLENAME + "." + Conversation.UUID + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + "," + Message.ENCRYPTION_PGP + "," + Message.ENCRYPTION_DECRYPTION_FAILED + "," + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + "," + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?"); if (uuid == null) { selectionArgs = new String[]{FtsUtils.toMatchString(term)}; } else { selectionArgs = new String[]{FtsUtils.toMatchString(term), uuid}; - SQL.append(" AND "+Conversation.TABLENAME+'.'+Conversation.UUID+"=?"); + SQL.append(" AND " + Conversation.TABLENAME + '.' + Conversation.UUID + "=?"); } SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS); Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term)); @@ -1118,23 +1155,23 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void deleteMessageInConversation(Message message) { long start = SystemClock.elapsedRealtime(); + final String uuid = message.getUuid(); final SQLiteDatabase db = this.getWritableDatabase(); db.beginTransaction(); ContentValues values = new ContentValues(); values.put(Message.DELETED, "1"); - String[] args = {message.getUuid()}; + String[] args = {uuid}; int rows = db.update("messages", values, "uuid =?", args); db.setTransactionSuccessful(); db.endTransaction(); - Log.d(Config.LOGTAG, "deleted " + rows + " message in " + (SystemClock.elapsedRealtime() - start) + "ms"); + Log.d(Config.LOGTAG, "deleted " + rows + " message (" + uuid + ") in " + (SystemClock.elapsedRealtime() - start) + "ms"); } public void deleteMessagesInConversation(Conversation conversation) { long start = SystemClock.elapsedRealtime(); final SQLiteDatabase db = this.getWritableDatabase(); db.beginTransaction(); - String[] args = {conversation.getUuid()}; - db.delete("messages_index", "uuid in (select uuid from messages where conversationUuid=?)", args); + final String[] args = {conversation.getUuid()}; int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); db.setTransactionSuccessful(); db.endTransaction(); @@ -1183,8 +1220,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { final String[] args = {String.valueOf(timestamp)}; SQLiteDatabase db = this.getReadableDatabase(); db.beginTransaction(); - db.delete("messages_index", "uuid in (select uuid from messages where timeSent mAccounts) { + try { + long start = SystemClock.elapsedRealtime(); + int num = 0; + if (dir == null) { + return; + } + Stack dirlist = new Stack(); + dirlist.clear(); + dirlist.push(dir); + File dirCurrent = dirlist.pop(); + File[] fileList = dirCurrent.listFiles(); + while (!dirlist.isEmpty()) { + if (fileList != null) { + for (File file : fileList) { + if (file.isDirectory()) { + dirlist.push(file); + } + } + } + } + if (fileList != null) { + ArrayList fileListByAccount = new ArrayList(); + ArrayList simpleFileList = new ArrayList(Arrays.asList(fileList)); + for (Account account : mAccounts) { + String jid = account.getJid().asBareJid().toString(); + for (int i = 0; i < simpleFileList.size(); i++) { + File currentFile = simpleFileList.get(i); + String fileName = currentFile.getName(); + if (fileName.startsWith(jid) && fileName.endsWith(".ceb")) { + fileListByAccount.add(currentFile); + simpleFileList.remove(currentFile); + i--; + } + } + if (fileListByAccount.size() > 2) { + num += expireOldBackups(fileListByAccount); + } + fileListByAccount.clear(); + } + } else { + return; + } + Log.d(Config.LOGTAG, "deleted " + num + " old backup files in " + (SystemClock.elapsedRealtime() - start) + "ms"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static int expireOldBackups(ArrayList fileListByAccount) { + int num = 0; + try { + Collections.sort(fileListByAccount, new Comparator() { + @Override + public int compare(File f1, File f2) { + return Long.compare(f2.lastModified(), f1.lastModified()); + } + }); + fileListByAccount.subList(0, 2).clear(); + for (File currentFile : fileListByAccount) { + if (currentFile.delete()) { + num++; + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + return num; + } + public void deleteFilesInDir(File dir) { long start = SystemClock.elapsedRealtime(); int num = 0; @@ -295,12 +379,11 @@ public class FileBackend { return; } Stack dirlist = new Stack<>(); - dirlist.clear(); dirlist.push(dir); while (!dirlist.isEmpty()) { File dirCurrent = dirlist.pop(); File[] fileList = dirCurrent.listFiles(); - if (fileList.length > 0) { + if (fileList != null && fileList.length > 0) { for (File file : fileList) { if (file.isDirectory()) { dirlist.push(file); @@ -462,20 +545,24 @@ public class FileBackend { public static String getConversationsDirectory(final String type) { if (type.equalsIgnoreCase("null")) { - return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator; + // return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator; + return INNER_APP_DIR[STORAGE_INDEX.get()] + + APP_DIRECTORY + File.separator; } else { return getAppMediaDirectory() + APP_DIRECTORY + " " + type + File.separator; } } public static String getAppMediaDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Media" + File.separator; + // return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Media" + File.separator; + return INNER_APP_DIR[STORAGE_INDEX.get()] + + APP_DIRECTORY + File.separator + "Media" + File.separator; } public static String getBackupDirectory(@Nullable String app) { if (app != null && (app.equalsIgnoreCase("conversations") || app.equalsIgnoreCase("Quicksy"))) { return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + app + "/Backup/"; - } else if (app != null && (app.equalsIgnoreCase("Monocles Messenger"))) { + } else if (app != null && (app.equalsIgnoreCase("Pix-Art Messenger"))) { return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + app + File.separator + "Database" + File.separator; } else { return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Database" + File.separator; @@ -600,6 +687,8 @@ public class FileBackend { ByteStreams.copy(is, os); } catch (IOException e) { throw new FileWriterException(); + } catch (Exception e) { + throw new FileWriterException(); } try { os.flush(); @@ -863,7 +952,7 @@ public class FileBackend { } DownloadableFile file = getFile(message); final String mime = file.getMimeType(); - if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) { + if ("application/pdf".equals(mime)) { thumbnail = getPDFPreview(file, size); } else if (mime.startsWith("video/")) { thumbnail = getVideoPreview(file, size); @@ -887,7 +976,6 @@ public class FileBackend { return thumbnail; } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap getPDFPreview(final File file, int size) { try { final ParcelFileDescriptor mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); @@ -1049,7 +1137,22 @@ public class FileBackend { return getUriForFile(mXmppConnectionService, file); } + public static Uri getUriForUri(Context context, Uri uri) { + if ("file".equals(uri.getScheme())) { + return getUriForFile(context, new File(uri.getPath())); + } else { + return uri; + } + } + public static Uri getUriForFile(Context context, File file) { + if (PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(SettingsActivity.USE_INNER_STORAGE, true)) { + File dataUser0File = new File(file.getAbsolutePath().replace("/data/data", "/data/user/0")); + return FileProvider.getUriForFile(context + , getAuthority(context) + , dataUser0File.exists() ? dataUser0File : file); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { return FileProvider.getUriForFile(context, getAuthority(context), file); @@ -1437,53 +1540,63 @@ public class FileBackend { final boolean apk = mime != null && mime.equals("application/vnd.android.package-archive"); final boolean pdf = "application/pdf".equals(mime); /* file params: - 1 | 2 | 3 | 4 | 5 | 6 - | image/video/pdf | a/v/gif | vcard/apk - url | filesize | width | height | runtime | name + 1 | 2 | 3 | 4 | 5 | 6 | + | image/video/pdf | a/v/gif | vcard/apk/audio | + url | filesize | width | height | runtime | name | */ final StringBuilder body = new StringBuilder(); if (url != null) { - body.append(url); + body.append(url); // 1 } - body.append('|').append(file.getSize()); - if (image || video || (pdf && Compatibility.runsTwentyOne())) { + body.append('|').append(file.getSize()); // 2 + if (image || video || pdf) { try { final Dimensions dimensions; if (video) { dimensions = getVideoDimensions(file); - } else if (pdf && Compatibility.runsTwentyOne()) { + } else if (pdf) { dimensions = getPDFDimensions(file); } else { dimensions = getImageDimensions(file); } if (dimensions.valid()) { - body.append('|').append(dimensions.width).append('|').append(dimensions.height); + body.append('|') + .append(dimensions.width) // 3 + .append('|') + .append(dimensions.height); // 4 if (isGif || video) { - body.append("|").append(getMediaRuntime(file, isGif)); + body.append("|").append(getMediaRuntime(file, isGif)); // 5 } } } catch (NotAVideoFile notAVideoFile) { Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file, trying to handle it as audio file"); try { - body.append("|0|0|").append(getMediaRuntime(file, false)).append('|').append(getAudioTitleArtist(file)); + body.append("|0|0|") // 3, 4 + .append(getMediaRuntime(file, false)) // 5 + .append('|') + .append(getAudioTitleArtist(file)); // 6 } catch (Exception e) { Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was neither a video file nor an audio file"); //fall threw } } } else if (audio) { - body.append("|0|0|").append(getMediaRuntime(file, false)).append('|').append(getAudioTitleArtist(file)); + body.append("|0|0|") // 3, 4 + .append(getMediaRuntime(file, false)) // 5 + .append('|') + .append(getAudioTitleArtist(file)); // 6 } else if (vcard) { - body.append("|0|0|0|").append(getVCard(file)); + body.append("|0|0|0|") // 3, 4, 5 + .append(getVCard(file)); // 6 } else if (apk) { - body.append("|0|0|0|").append(getAPK(file, mXmppConnectionService.getApplicationContext())); + body.append("|0|0|0|") // 3, 4, 5 + .append(getAPK(file, mXmppConnectionService.getApplicationContext())); // 6 } message.setBody(body.toString()); message.setFileDeleted(false); message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Dimensions getPDFDimensions(final File file) { final ParcelFileDescriptor fileDescriptor; try { @@ -1516,16 +1629,16 @@ public class FileBackend { public int getMediaRuntime(File file, boolean isGif) { if (isGif) { - try { - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(getUriForFile(mXmppConnectionService, file)); - Movie movie = Movie.decodeStream(inputStream); - int duration = movie.duration(); - close(inputStream); - return duration; - } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, "unable to get image dimensions", e); - return 0; - } + try { + final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(getUriForFile(mXmppConnectionService, file)); + Movie movie = Movie.decodeStream(inputStream); + int duration = movie.duration(); + close(inputStream); + return duration; + } catch (FileNotFoundException e) { + Log.d(Config.LOGTAG, "unable to get image dimensions", e); + return 0; + } } else { try { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); @@ -1661,7 +1774,7 @@ public class FileBackend { return bitmap; } DownloadableFile file = new DownloadableFile(attachment.getUri().getPath()); - if ("application/pdf".equals(attachment.getMime()) && Compatibility.runsTwentyOne()) { + if ("application/pdf".equals(attachment.getMime())) { bitmap = cropCenterSquare(getPDFPreview(file, size), size); } else if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) { bitmap = cropCenterSquareVideo(attachment.getUri(), size); @@ -1719,11 +1832,7 @@ public class FileBackend { return dimensions; } final int rotation; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { - rotation = extractRotationFromMediaRetriever(metadataRetriever); - } else { - rotation = 0; - } + rotation = extractRotationFromMediaRetriever(metadataRetriever); boolean rotated = rotation == 90 || rotation == 270; int height; try { @@ -1744,7 +1853,6 @@ public class FileBackend { return rotated ? new Dimensions(width, height) : new Dimensions(height, width); } - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) { String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); try { @@ -1851,30 +1959,11 @@ public class FileBackend { public static boolean weOwnFile(Context context, Uri uri) { if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return false; - } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return fileIsInFilesDir(context, uri); } else { return weOwnFileLollipop(uri); } } - - /** - * This is more than hacky but probably way better than doing nothing - * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir - * and check against those as well - */ - private static boolean fileIsInFilesDir(Context context, Uri uri) { - try { - final String haystack = context.getFilesDir().getParentFile().getCanonicalPath(); - final String needle = new File(uri.getPath()).getCanonicalPath(); - return needle.startsWith(haystack); - } catch (IOException e) { - return false; - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static boolean weOwnFileLollipop(Uri uri) { try { File file = new File(uri.getPath()); @@ -1944,11 +2033,7 @@ public class FileBackend { } public static String getGlobalDocumentsPath() { - if (Compatibility.runsNineTeen()) { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/monocles chat/"; - } else { - return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Documents"; - } + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/monocles chat/"; } public static String getGlobalAudiosPath() { @@ -1956,14 +2041,21 @@ public class FileBackend { } public void saveFile(final Message message, final Activity activity) { - final DownloadableFile source = getFile(message); - final File destination = new File(getDestinationToSaveFile(message)); - try { - copyFile(source, destination); - ToastCompat.makeText(activity, activity.getString(R.string.file_copied_to, destination), ToastCompat.LENGTH_SHORT).show(); - } catch (IOException e) { - e.printStackTrace(); - } + new Thread(() -> { + final DownloadableFile source = getFile(message); + final File destination = new File(getDestinationToSaveFile(message)); + try { + activity.runOnUiThread(() -> { + ToastCompat.makeText(activity, activity.getString(R.string.copy_file_to, destination), ToastCompat.LENGTH_SHORT).show(); + }); + copyFile(source, destination); + activity.runOnUiThread(() -> { + ToastCompat.makeText(activity, activity.getString(R.string.file_copied_to, destination), ToastCompat.LENGTH_SHORT).show(); + }); + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); } public String getDestinationToSaveFile(Message message) { diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index 51f3d7141..96d5dca01 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.services; -import android.content.Context; +import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS; + import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; @@ -35,8 +36,6 @@ import okio.BufferedSink; import okio.Okio; import okio.Source; -import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS; - public class AbstractConnectionManager { private static final int UI_REFRESH_THRESHOLD = Config.REFRESH_UI_INTERVAL; private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0); @@ -119,9 +118,9 @@ public class AbstractConnectionManager { } public long getAutoAcceptFileSize() { - long defaultValue_wifi = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_wifi); - long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile); - long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming); + final long defaultValue_wifi = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_wifi); + final long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile); + final long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming); String config = "0"; if (mXmppConnectionService.isWIFI()) { @@ -135,7 +134,7 @@ public class AbstractConnectionManager { "auto_accept_file_size_roaming", String.valueOf(defaultValue_roaming)); } try { - return Long.parseLong(config); + return Long.parseLong(config) <= 0 ? -1 : Long.parseLong(config); } catch (NumberFormatException e) { return defaultValue_mobile; } @@ -154,7 +153,6 @@ public class AbstractConnectionManager { } } - public PowerManager.WakeLock createWakeLock(final String name) { final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class); return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index f0996e893..ea4421464 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -4,15 +4,17 @@ import android.content.Context; import android.content.SharedPreferences; import android.media.MediaMetadataRetriever; import android.net.Uri; -import android.os.Build; import android.os.ParcelFileDescriptor; import android.preference.PreferenceManager; import android.util.Log; -import androidx.annotation.RequiresApi; +import androidx.annotation.NonNull; -import net.ypresto.androidtranscoder.MediaTranscoder; -import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets; +import com.otaliastudios.transcoder.Transcoder; +import com.otaliastudios.transcoder.TranscoderListener; +import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy; + +import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.FileDescriptor; @@ -20,6 +22,7 @@ import java.io.FileNotFoundException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -32,8 +35,9 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.utils.MimeUtils; +import eu.siacs.conversations.utils.TranscoderStrategies; -public class AttachFileToConversationRunnable implements Runnable, MediaTranscoder.Listener { +public class AttachFileToConversationRunnable implements Runnable, TranscoderListener { private final XmppConnectionService mXmppConnectionService; private final Message message; @@ -56,16 +60,15 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod this.callback = callback; this.maxUploadSize = maxUploadSize; final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); - final int autoAcceptFileSize = Config.FILE_SIZE; this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri); this.isVideoMessage = !getFileBackend().useFileAsIs(uri) - && (mimeType != null && mimeType.startsWith("video/") - && (mXmppConnectionService.getCompressVideoBitratePreference() != 0 && mXmppConnectionService.getCompressVideoResolutionPreference() != 0)) - && originalFileSize > autoAcceptFileSize; + && (mimeType != null && mimeType.startsWith("video/")) + && originalFileSize > Config.VIDEO_FAST_UPLOAD_SIZE + && !"uncompressed".equals(getVideoCompression()); } boolean isVideoMessage() { - return this.isVideoMessage && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; + return this.isVideoMessage; } private void processAsFile() { @@ -100,21 +103,22 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod } } - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) private void processAsVideo() throws Exception { Log.d(Config.LOGTAG, "processing file as video"); mXmppConnectionService.startForcingForegroundNotification(); isCompressingVideo = conversation.getUuid(); - SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); message.setRelativeFilePath("Sent/" + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + "_komp.mp4"); final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); - file.getParentFile().mkdirs(); + if (Objects.requireNonNull(file.getParentFile()).mkdirs()) { + Log.d(Config.LOGTAG, "created parent directory for video file"); + } final ParcelFileDescriptor parcelFileDescriptor = mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r"); if (parcelFileDescriptor == null) { throw new FileNotFoundException("Parcel File Descriptor was null"); } - FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - Future future = getVideoCompressor(fileDescriptor, file, maxUploadSize); + final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); + final Future future = getVideoCompression(fileDescriptor, file, maxUploadSize, originalFileSize); try { future.get(); } catch (InterruptedException e) { @@ -127,13 +131,13 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod isCompressingVideo = null; processAsFile(); } else { - Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e); + Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFailed() instead", e); } } } - private Future getVideoCompressor(final FileDescriptor fileDescriptor, final File file, final long maxUploadSize) throws Exception { - MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + private Future getVideoCompression(final FileDescriptor fileDescriptor, final File file, final long maxUploadSize, final long originalFileSize) throws Exception { + final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); try { mediaMetadataRetriever.setDataSource(fileDescriptor); } catch (Exception e) { @@ -141,31 +145,52 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod throw new Exception(e); } long videoDuration; - long estimatedFileSize = maxUploadSize / 2; // keep estimated filesize half as big as maxUploadSize + final long estimatedFileSize = maxUploadSize / 2; // keep estimated filesize half as big as maxUploadSize try { videoDuration = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) / 1000; //in seconds } catch (NumberFormatException e) { videoDuration = -1; } - int bitrateAfterCompression = safeLongToInt(mXmppConnectionService.getCompressVideoBitratePreference() / 8); //in bytes - long size = videoDuration * bitrateAfterCompression; - if (estimatedFileSize >= size) { - return MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), MediaFormatStrategyPresets.createAndroidStandardStrategy(mXmppConnectionService.getCompressVideoBitratePreference(), mXmppConnectionService.getCompressVideoResolutionPreference()), this); + final int bitrateAfterCompression = safeLongToInt(mXmppConnectionService.getCompressVideoBitratePreference() / 8); //in bytes + final long sizeAfterCompression = videoDuration * bitrateAfterCompression; + + if (sizeAfterCompression >= originalFileSize && originalFileSize <= Config.VIDEO_FAST_UPLOAD_SIZE) { + processAsFile(); + onTranscodeCanceled(); + return null; + } else if (estimatedFileSize >= sizeAfterCompression && sizeAfterCompression <= originalFileSize) { + return Transcoder.into(file.getAbsolutePath()). + addDataSource(mXmppConnectionService, uri) + .setVideoTrackStrategy(TranscoderStrategies.VIDEO(mXmppConnectionService.getCompressVideoBitratePreference(), mXmppConnectionService.getCompressVideoResolutionPreference())) + .setAudioTrackStrategy(mXmppConnectionService.getCompressAudioPreference()) + .setListener(this) + .transcode(); } else { + DefaultAudioStrategy audioStrategy; int newBitrate = safeLongToInt((estimatedFileSize / videoDuration) * 8); // in bits/sec int newResoloution = 0; if (newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_bitrate)) { newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_res); + audioStrategy = TranscoderStrategies.AUDIO_LQ; } else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.low_video_bitrate)) { newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.low_video_res); + audioStrategy = TranscoderStrategies.AUDIO_LQ; } else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.low_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.mid_video_bitrate)) { newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.mid_video_res); + audioStrategy = TranscoderStrategies.AUDIO_MQ; } else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.mid_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.high_video_bitrate)) { newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.high_video_res); + audioStrategy = TranscoderStrategies.AUDIO_HQ; } else { newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.high_video_res); + audioStrategy = TranscoderStrategies.AUDIO_HQ; } - return MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), MediaFormatStrategyPresets.createAndroidStandardStrategy(newBitrate, newResoloution), this); + return Transcoder.into(file.getAbsolutePath()). + addDataSource(mXmppConnectionService, uri) + .setVideoTrackStrategy(TranscoderStrategies.VIDEO(newBitrate, newResoloution)) + .setAudioTrackStrategy(audioStrategy) + .setListener(this) + .transcode(); } } @@ -187,7 +212,7 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod } @Override - public void onTranscodeCompleted() { + public void onTranscodeCompleted(int successCode) { mXmppConnectionService.stopForcingForegroundNotification(); isCompressingVideo = null; final File file = mXmppConnectionService.getFileBackend().getFile(message); @@ -219,10 +244,10 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod } @Override - public void onTranscodeFailed(Exception e) { + public void onTranscodeFailed(@NonNull @NotNull Throwable exception) { mXmppConnectionService.stopForcingForegroundNotification(); isCompressingVideo = null; - Log.d(Config.LOGTAG, "video transcoding failed", e); + Log.d(Config.LOGTAG, "video transcoding failed", exception); processAsFile(); } @@ -240,6 +265,10 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod } } + private String getVideoCompression() { + return getVideoCompression(mXmppConnectionService); + } + public static String getVideoCompression(final Context context) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression)); diff --git a/src/main/java/eu/siacs/conversations/services/AudioPlayer.java b/src/main/java/eu/siacs/conversations/services/AudioPlayer.java index 4e814e0c6..12530a4d8 100644 --- a/src/main/java/eu/siacs/conversations/services/AudioPlayer.java +++ b/src/main/java/eu/siacs/conversations/services/AudioPlayer.java @@ -101,17 +101,10 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti private boolean init(ViewHolder viewHolder, Message message) { messageAdapter.getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC); - 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); - } viewHolder.progress.setOnSeekBarChangeListener(this); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - ColorStateList color = ThemeHelper.AudioPlayerColor(messageAdapter.getContext()); - viewHolder.progress.setThumbTintList(color); - viewHolder.progress.setProgressTintList(color); - } + ColorStateList color = ThemeHelper.AudioPlayerColor(messageAdapter.getContext()); + viewHolder.progress.setThumbTintList(color); + viewHolder.progress.setProgressTintList(color); viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f); viewHolder.playPause.setOnClickListener(this); if (message == currentlyPlayingMessage) { @@ -439,6 +432,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti Log.i(Config.LOGTAG, "Audio focus failed."); } } + public static class ViewHolder { private TextView runtime; private SeekBar progress; diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index f90602ed7..083e69320 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -50,7 +50,7 @@ public class ChannelDiscoveryService { } void initializeMuclumbusService() { - final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); + final OkHttpClient.Builder builder = new OkHttpClient.Builder(); if (service.useTorToConnect()) { builder.proxy(HttpConnectionManager.getProxy()); } diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index d1e87ab27..8c4729531 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.services.NotificationService.EXPORT_BACKUP_NOTIFICATION_ID; import static eu.siacs.conversations.utils.Compatibility.runsTwentySix; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -17,8 +19,6 @@ import android.os.IBinder; import android.os.PowerManager; import android.preference.PreferenceManager; import android.util.Log; -import java.io.FileNotFoundException; -import java.io.ObjectOutputStream; import androidx.core.app.NotificationCompat; @@ -81,7 +81,6 @@ public class ExportBackupService extends Service { private static final String DIRECTORY_STRING_FORMAT = FileBackend.getAppLogsDirectory() + "%s"; private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n"; - private static final int NOTIFICATION_ID = 19; private static final int PAGE_SIZE = 20; private static final AtomicBoolean RUNNING = new AtomicBoolean(false); private DatabaseBackend mDatabaseBackend; @@ -271,6 +270,7 @@ public class ExportBackupService extends Service { RUNNING.set(false); if (success) { notifySuccess(files, notify); + FileBackend.deleteOldBackups(new File(FileBackend.getBackupDirectory(null)), this.mAccounts); } else { notifyError(); } @@ -313,7 +313,7 @@ public class ExportBackupService extends Service { final int percentage = i * 100 / size; if (p < percentage) { p = percentage; - notificationManager.notify(NOTIFICATION_ID, progress.build(p)); + notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, progress.build(p)); } } if (cursor != null) { @@ -327,7 +327,7 @@ public class ExportBackupService extends Service { mBuilder.setContentTitle(getString(R.string.notification_create_backup_title)) .setSmallIcon(R.drawable.ic_archive_white_24dp) .setProgress(1, 0, false); - startForeground(NOTIFICATION_ID, mBuilder.build()); + startForeground(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); int count = 0; final int max = this.mAccounts.size(); final SecureRandom secureRandom = new SecureRandom(); @@ -344,55 +344,58 @@ public class ExportBackupService extends Service { final List files = new ArrayList<>(); Log.d(Config.LOGTAG, "starting backup for " + max + " accounts"); for (final Account account : this.mAccounts) { - final String password = account.getPassword(); - if (Strings.nullToEmpty(password).trim().isEmpty()) { - Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid())); - continue; - } - Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid())); - final byte[] IV = new byte[12]; - final byte[] salt = new byte[16]; - secureRandom.nextBytes(IV); - secureRandom.nextBytes(salt); - final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt); - final Progress progress = new Progress(mBuilder, max, count); - final File file = new File(FileBackend.getBackupDirectory(null) + account.getJid().asBareJid().toEscapedString() + ".ceb"); - files.add(file); - final File directory = file.getParentFile(); - if (directory != null && directory.mkdirs()) { - Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); - } - final FileOutputStream fileOutputStream = new FileOutputStream(file); - final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); - backupFileHeader.write(dataOutputStream); - dataOutputStream.flush(); + try { + final String password = account.getPassword(); + if (Strings.nullToEmpty(password).trim().isEmpty()) { + Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid())); + continue; + } + Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid())); + final byte[] IV = new byte[12]; + final byte[] salt = new byte[16]; + secureRandom.nextBytes(IV); + secureRandom.nextBytes(salt); + final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt); + final Progress progress = new Progress(mBuilder, max, count); + final File file = new File(FileBackend.getBackupDirectory(null) + account.getJid().asBareJid().toEscapedString() + "_" + ((new SimpleDateFormat("yyyy-MM-dd")).format(new Date())) + ".ceb"); + files.add(file); + final File directory = file.getParentFile(); + if (directory != null && directory.mkdirs()) { + Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); + } + final FileOutputStream fileOutputStream = new FileOutputStream(file); + final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + backupFileHeader.write(dataOutputStream); + dataOutputStream.flush(); - final Cipher cipher = Compatibility.runsTwentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); - final byte[] key = getKey(password, salt); - Log.d(Config.LOGTAG, backupFileHeader.toString()); - SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(IV); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); - - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); - PrintWriter writer = new PrintWriter(gzipOutputStream); - SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase(); - final String uuid = account.getUuid(); - accountExport(db, uuid, writer); - simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer); - messageExport(db, uuid, writer, progress); - for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { - simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer); + final Cipher cipher = Compatibility.runsTwentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); + final byte[] key = getKey(password, salt); + Log.d(Config.LOGTAG, backupFileHeader.toString()); + SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); + CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); + PrintWriter writer = new PrintWriter(gzipOutputStream); + SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase(); + final String uuid = account.getUuid(); + accountExport(db, uuid, writer); + simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer); + messageExport(db, uuid, writer, progress); + for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { + simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer); + } + writer.flush(); + writer.close(); + mediaScannerScanFile(file); + Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); + } catch (Exception e) { + Log.d(Config.LOGTAG, "backup for " + account.getJid() + " failed with " + e); } - writer.flush(); - writer.close(); - mediaScannerScanFile(file); - Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); count++; } stopForeground(true); - notificationManager.cancel(NOTIFICATION_ID); + notificationManager.cancel(EXPORT_BACKUP_NOTIFICATION_ID); return files; } @@ -465,7 +468,7 @@ public class ExportBackupService extends Service { if (shareFilesIntent != null) { mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent); } - notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); } private void notifyError() { @@ -477,7 +480,7 @@ public class ExportBackupService extends Service { .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_failed_subtitle, FileBackend.getBackupDirectory(null)))) .setAutoCancel(true) .setSmallIcon(R.drawable.ic_warning_white_24dp); - notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); } private void writeToFile(Conversation conversation) { diff --git a/src/main/java/eu/siacs/conversations/services/ImportBackupService.java b/src/main/java/eu/siacs/conversations/services/ImportBackupService.java index eed3bf0dc..68fc5b9c3 100644 --- a/src/main/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ImportBackupService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.services.NotificationService.IMPORT_BACKUP_NOTIFICATION_ID; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -60,7 +62,6 @@ import eu.siacs.conversations.xmpp.Jid; public class ImportBackupService extends Service { - private static final int NOTIFICATION_ID = 21; private static final AtomicBoolean running = new AtomicBoolean(false); private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder(); private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName()); @@ -162,7 +163,7 @@ public class ImportBackupService extends Service { } private void startForegroundService() { - startForeground(NOTIFICATION_ID, createImportBackupNotification(1, 0)); + startForeground(IMPORT_BACKUP_NOTIFICATION_ID, createImportBackupNotification(1, 0)); } private void updateImportBackupNotification(final long total, final long current) { @@ -177,7 +178,7 @@ public class ImportBackupService extends Service { } final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); try { - notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress)); + notificationManager.notify(IMPORT_BACKUP_NOTIFICATION_ID, createImportBackupNotification(max, progress)); } catch (final RuntimeException e) { Log.d(Config.LOGTAG, "unable to make notification", e); } @@ -303,7 +304,7 @@ public class ImportBackupService extends Service { .setAutoCancel(true) .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)) .setSmallIcon(R.drawable.ic_unarchive_white_24dp); - notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + notificationManager.notify(IMPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); } private void stopBackgroundService() { diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index 0ac42e5eb..1ed64ee98 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -67,8 +67,8 @@ import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Enumeration; -import java.util.Locale; import java.util.List; +import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; @@ -95,6 +95,7 @@ import eu.siacs.conversations.ui.MemorizingActivity; * opening sockets! */ public class MemorizingTrustManager { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); final static String DECISION_INTENT = "de.duenndns.ssl.DECISION"; @@ -358,6 +359,7 @@ public class MemorizingTrustManager { } } + private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive) throws CertificateException { LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); @@ -525,6 +527,7 @@ public class MemorizingTrustManager { } private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) { + si.append("\n"); if (showValidFor) { try { @@ -574,6 +577,7 @@ public class MemorizingTrustManager { } return si.toString(); } + /** * Returns the top-most entry of the activity stack. * @@ -646,6 +650,7 @@ public class MemorizingTrustManager { public X509TrustManager getInteractive() { return new InteractiveMemorizingTrustManager(null); } + private class NonInteractiveMemorizingTrustManager implements X509TrustManager { private final String domain; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index a429062da..9942e42e2 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -229,31 +229,31 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { private void execute(final Query query) { final Account account = query.getAccount(); if (account.getStatus() == Account.State.ONLINE) { - final Conversation conversation = query.getConversation(); - if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - throw new IllegalStateException("Attempted to run MAM query for archived conversation"); - } + final Conversation conversation = query.getConversation(); + if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) { + throw new IllegalStateException("Attempted to run MAM query for archived conversation"); + } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString()); - final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); + final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> { - final Element fin = p.findChild("fin", query.version.namespace); + final Element fin = p.findChild("fin", query.version.namespace); if (p.getType() == IqPacket.TYPE.TIMEOUT) { - synchronized (this.queries) { - this.queries.remove(query); + synchronized (this.queries) { + this.queries.remove(query); if (query.hasCallback()) { query.callback(false); } } } else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) { - final boolean running; - synchronized (this.queries) { - running = this.queries.contains(query); - } - if (running) { - processFin(query, fin); - } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring MAM iq result because query had been killed"); - } + final boolean running; + synchronized (this.queries) { + running = this.queries.contains(query); + } + if (running) { + processFin(query, fin); + } else { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring MAM iq result because query had been killed"); + } } else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) { //do nothing } else { @@ -268,11 +268,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - private void finalizeQuery(final Query query, boolean done) { + private void finalizeQuery(final Query query, boolean done) { synchronized (this.queries) { - if (!this.queries.remove(query)) { - throw new IllegalStateException("Unable to remove query from queries"); - } + if (!this.queries.remove(query)) { + throw new IllegalStateException("Unable to remove query from queries"); + } } final Conversation conversation = query.getConversation(); if (conversation != null) { @@ -403,7 +403,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } synchronized (this.queries) { - for (final Query q : queries) { + for (final Query q : queries) { if (q.conversation == conversation) { toBeKilled.add(q); } @@ -627,7 +627,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - @NotNull + @NotNull @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 0c75ff8f4..b3d08d274 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -34,6 +36,8 @@ import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.IconCompat; +import com.google.common.collect.Iterables; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -72,8 +76,6 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.Media; -import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; - public class NotificationService { public static final Object CATCHUP_LOCK = new Object(); @@ -106,6 +108,8 @@ public class NotificationService { public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 14; + public static final int IMPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 16; + public static final int EXPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 18; private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final LinkedHashMap mMissedCalls = new LinkedHashMap<>(); @@ -730,14 +734,23 @@ public class NotificationService { notify(INCOMING_CALL_NOTIFICATION_ID, notification); } - public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set media) { + public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) { + final AbstractJingleConnection.Id id = ongoingCall.id; final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, ONGOING_CALLS_CHANNEL_ID); - if (media.contains(Media.VIDEO)) { + if (ongoingCall.media.contains(Media.VIDEO)) { builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + if (ongoingCall.reconnecting) { + builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call)); + } else { + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + } } else { builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + if (ongoingCall.reconnecting) { + builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call)); + } else { + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + } } builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); @@ -1214,17 +1227,18 @@ public class NotificationService { .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) .setShowsUserInterface(false) .build(); - String replyLabel = mXmppConnectionService.getString(R.string.reply); - NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( + final String replyLabel = mXmppConnectionService.getString(R.string.reply); + final String lastMessageUuid = Iterables.getLast(messages).getUuid(); + final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( R.drawable.ic_reply_white_24dp, replyLabel, - createReplyIntent(conversation, false)) + createReplyIntent(conversation, lastMessageUuid, false)) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) .setShowsUserInterface(false) .addRemoteInput(remoteInput).build(); - NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, + final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, replyLabel, - createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build(); + createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build(); mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction)); int addedActionsCount = 1; mBuilder.addAction(markReadAction); @@ -1505,13 +1519,14 @@ public class NotificationService { return PendingIntent.getService(mXmppConnectionService, 1, intent, 0); } - private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) { + private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION); intent.putExtra("uuid", conversation.getUuid()); intent.putExtra("dismiss_notification", dismissAfterReply); + intent.putExtra("last_message_uuid", lastMessageUuid); final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14); - return PendingIntent.getService(mXmppConnectionService, id, intent, 0); + return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReadPendingIntent(Conversation conversation) { diff --git a/src/main/java/eu/siacs/conversations/services/ProviderService.java b/src/main/java/eu/siacs/conversations/services/ProviderService.java new file mode 100644 index 000000000..78f7726fa --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/ProviderService.java @@ -0,0 +1,117 @@ +package eu.siacs.conversations.services; + +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.http.HttpConnectionManager; + +public class ProviderService extends AsyncTask { + public static List providers = new ArrayList<>(); + + // in accordance with cat B (https://invent.kde.org/melvo/xmpp-providers/) + public static boolean REGISTRATION = true; + public static boolean FREE = true; + public static int COMPLIANCE = 90; + public static String RATING = "A"; + + public ProviderService() { + } + + public static List getProviders() { + final HashSet provider = new HashSet<>(Config.DOMAIN.DOMAINS); + if (!providers.isEmpty()) { + provider.addAll(providers); + } + return new ArrayList<>(provider); + } + + @Override + protected Boolean doInBackground(String... params) { + StringBuilder jsonString = new StringBuilder(); + boolean isError = false; + try { + Log.d(Config.LOGTAG, "ProviderService: Updating provider list from " + Config.PROVIDER_URL); + final InputStream is = HttpConnectionManager.open(Config.PROVIDER_URL, false); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = reader.readLine()) != null) { + jsonString.append(line); + } + is.close(); + reader.close(); + } catch (Exception e) { + e.printStackTrace(); + isError = true; + } + + try { + parseJson(new JSONObject(jsonString.toString())); + } catch (JSONException e) { + e.printStackTrace(); + isError = true; + } + if (isError) { + Log.d(Config.LOGTAG, "ProviderService: Updating provider list failed"); + } + return !isError; + } + + private void parseJson(JSONObject jsonObject) { + if (jsonObject != null) { + try { + for (int i = 0; i < jsonObject.length(); i++) { + boolean inBandRegistration = false; + boolean freeOfCharge = false; + String ratingC2S = null; + String ratingS2S = null; + int ratingXmppComplianceTester = 0; + final String provider = jsonObject.names().getString(i); + if (provider.length() > 0) { + for (int ii = 0; ii < jsonObject.length(); ii++) { + final JSONObject json = new JSONObject(jsonObject.get(provider).toString()); + String featureName = json.names().getString(ii); + final JSONObject subjson = new JSONObject(json.get(json.names().getString(ii)).toString()); + if (featureName.equals("inBandRegistration")) { + inBandRegistration = subjson.getBoolean("content"); + } + if (featureName.equals("ratingXmppComplianceTester")) { + ratingXmppComplianceTester = subjson.getInt("content"); + } + if (featureName.equals("freeOfCharge")) { + freeOfCharge = subjson.getBoolean("content"); + } + if (featureName.equals("ratingImObservatoryClientToServer")) { + ratingC2S = subjson.getString("content"); + } + if (featureName.equals("ratingImObservatoryServerToServer")) { + ratingS2S = subjson.getString("content"); + } + if (!Config.DOMAIN.BLACKLISTED_DOMAINS.contains(provider) + && inBandRegistration == REGISTRATION + && ratingXmppComplianceTester >= COMPLIANCE + && freeOfCharge == FREE + && (ratingC2S != null && ratingC2S.equalsIgnoreCase(RATING)) + && (ratingS2S != null && ratingS2S.equalsIgnoreCase(RATING))) { + //Log.d(Config.LOGTAG, "ProviderService: Updating provider list. Adding " + provider + " (Registration: " + inBandRegistration + " Compliance: " + ratingXmppComplianceTester + " Free: " + freeOfCharge + " Rating C2S/S2S: " + ratingC2S + "/" + ratingS2S + ")"); + providers.add(provider); + } + } + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/UpdateService.java b/src/main/java/eu/siacs/conversations/services/UpdateService.java new file mode 100644 index 000000000..53bbb8de7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/UpdateService.java @@ -0,0 +1,227 @@ +package eu.siacs.conversations.services; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +import eu.siacs.conversations.BuildConfig; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.http.NoSSLv3SocketFactory; +import eu.siacs.conversations.ui.UpdaterActivity; +import me.drakeet.support.toast.ToastCompat; + +import static eu.siacs.conversations.http.HttpConnectionManager.getProxy; + +public class UpdateService extends AsyncTask { + private boolean mUseTor; + private Context context; + private String store; + private NotificationService getNotificationService; + + public UpdateService() { + } + + public UpdateService(Context context, String Store, XmppConnectionService mXmppConnectionService) { + this.context = context; + this.store = Store; + this.mUseTor = mXmppConnectionService.useTorToConnect(); + this.getNotificationService = mXmppConnectionService.getNotificationService(); + } + + @Override + protected Wrapper doInBackground(String... params) { + StringBuilder jsonString = new StringBuilder(); + boolean UpdateAvailable = false; + boolean showNoUpdateToast = false; + boolean isError = false; + + if (params[0].equals("true")) { + showNoUpdateToast = true; + } + SSLContext sslcontext = null; + SSLSocketFactory NoSSLv3Factory = null; + try { + sslcontext = SSLContext.getInstance("TLSv1"); + if (sslcontext != null) { + sslcontext.init(null, null, null); + NoSSLv3Factory = new NoSSLv3SocketFactory(sslcontext.getSocketFactory()); + } + } catch (NoSuchAlgorithmException | KeyManagementException e) { + e.printStackTrace(); + } + HttpsURLConnection.setDefaultSSLSocketFactory(NoSSLv3Factory); + HttpsURLConnection connection = null; + try { + URL url = new URL(Config.UPDATE_URL); + if (mUseTor) { + connection = (HttpsURLConnection) url.openConnection(getProxy()); + } else { + connection = (HttpsURLConnection) url.openConnection(); + } + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setRequestProperty("User-agent", System.getProperty("http.agent")); + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + jsonString.append(line); + } + } catch (Exception e) { + e.printStackTrace(); + isError = true; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + + try { + JSONObject json = new JSONObject(jsonString.toString()); + if (json.getBoolean("success") && json.has("latestVersion") && json.has("appURI") && json.has("filesize")) { + String version = json.getString("latestVersion"); + String ownVersion = BuildConfig.VERSION_NAME; + String url = json.getString("appURI"); + String filesize = json.getString("filesize"); + String changelog = ""; + if (json.has("changelog")) { + changelog = json.getString("changelog"); + } + if (checkVersion(version, ownVersion) >= 1) { + Log.d(Config.LOGTAG, "AppUpdater: Version " + ownVersion + " should be updated to " + version); + UpdateAvailable = true; + showNotification(url, changelog, version, filesize, store); + } else { + Log.d(Config.LOGTAG, "AppUpdater: Version " + ownVersion + " is up to date"); + UpdateAvailable = false; + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + Wrapper w = new Wrapper(); + w.isError = isError; + w.UpdateAvailable = UpdateAvailable; + w.NoUpdate = showNoUpdateToast; + return w; + } + + @Override + protected void onPostExecute(Wrapper w) { + super.onPostExecute(w); + if (w.isError) { + showToastMessage(true, true); + return; + } + if (!w.UpdateAvailable) { + showToastMessage(w.NoUpdate, false); + } + } + + private void showToastMessage(boolean show, final boolean error) { + if (!show) { + return; + } + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> { + String ToastMessage = ""; + if (error) { + ToastMessage = context.getString(R.string.failed); + } else { + ToastMessage = context.getString(R.string.no_update_available); + } + ToastCompat.makeText(context, ToastMessage, ToastCompat.LENGTH_LONG).show(); + }); + } + + private void showNotification(String url, String changelog, String version, String filesize, String store) { + Intent intent = new Intent(context, UpdaterActivity.class); + intent.putExtra("update", "MonoclesMessenger_UpdateService"); + intent.putExtra("url", url); + intent.putExtra("changelog", changelog); + intent.putExtra("store", store); + PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + getNotificationService.AppUpdateServiceNotification(getNotificationService.AppUpdateNotification(pi, version, filesize)); + } + + private int checkVersion(String remoteVersion, String installedVersion) { + // Use this instead of String.compareTo() for a non-lexicographical + // comparison that works for version strings. e.g. "1.10".compareTo("1.6"). + // + // @param str1 a string of ordinal numbers separated by decimal points. + // @param str2 a string of ordinal numbers separated by decimal points. + // @return The result is a negative integer if str1 is _numerically_ less than str2. + // The result is a positive integer if str1 is _numerically_ greater than str2. + // The result is zero if the strings are _numerically_ equal. + // @note It does not work if "1.10" is supposed to be equal to "1.10.0". + + String[] remote = null; + String[] installed = null; + String[] remoteV = null; + String[] installedV = null; + try { + installedV = installedVersion.split("[ |\\-]"); + Log.d(Config.LOGTAG, "AppUpdater: Version installed: " + installedV[0]); + installed = installedV[0].split("\\."); + } catch (Exception e) { + e.printStackTrace(); + } + try { + remoteV = remoteVersion.split(" "); + if (installedV != null && installedV.length > 1) { + if (installedV[1] != null && installedV[1].toLowerCase().contains("beta")) { + remoteV[0] = remoteV[0] + ".1"; + } + } + Log.d(Config.LOGTAG, "AppUpdater: Version on server: " + remoteV[0]); + remote = remoteV[0].split("\\."); + } catch (Exception e) { + e.printStackTrace(); + } + int i = 0; + // set index to first non-equal ordinal or length of shortest localVersion string + try { + if (remote != null && installed != null) { + while (i < remote.length && i < installed.length && remote[i].equals(installed[i])) { + i++; + } + // compare first non-equal ordinal number + if (i < remote.length && i < installed.length) { + int diff = Integer.valueOf(remote[i]).compareTo(Integer.valueOf(installed[i])); + return Integer.signum(diff); + } + // the strings are equal or one string is a substring of the other + // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" + return Integer.signum(remote.length - installed.length); + } + } catch (Exception e) { + showToastMessage(true, true); + e.printStackTrace(); + return 0; + } + return 0; + } + + class Wrapper { + boolean UpdateAvailable = false; + boolean NoUpdate = false; + boolean isError = false; + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 5f67407d1..ab025ab59 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,5 +1,16 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_CORRECTION; +import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_RETRACTION; +import static eu.siacs.conversations.ui.SettingsActivity.AUTOMATIC_ATTACHMENT_DELETION; +import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES; +import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES; +import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_MULTI_ACCOUNTS; +import static eu.siacs.conversations.ui.SettingsActivity.INDICATE_RECEIVED; +import static eu.siacs.conversations.ui.SettingsActivity.SHOW_OWN_ACCOUNTS; +import static eu.siacs.conversations.ui.SettingsActivity.USE_INNER_STORAGE; +import static eu.siacs.conversations.utils.RichPreview.RICH_LINK_METADATA; + import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -43,15 +54,6 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.LruCache; import android.util.Pair; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.SessionImpl; -import net.java.otr4j.session.SessionStatus; -import eu.siacs.conversations.xmpp.jid.OtrJidHelper; - import androidx.annotation.BoolRes; import androidx.annotation.IntegerRes; @@ -60,6 +62,7 @@ import androidx.core.content.ContextCompat; import com.google.common.base.Objects; import com.google.common.base.Strings; +import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy; import org.conscrypt.Conscrypt; import org.openintents.openpgp.IOpenPgpService2; @@ -90,6 +93,8 @@ import java.util.TimeZone; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; @@ -151,6 +156,7 @@ import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.TorServiceUtils; +import eu.siacs.conversations.utils.TranscoderStrategies; import eu.siacs.conversations.utils.WakeLockHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; @@ -182,15 +188,6 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import me.leolin.shortcutbadger.ShortcutBadger; -import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_CORRECTION; -import static eu.siacs.conversations.ui.SettingsActivity.AUTOMATIC_ATTACHMENT_DELETION; -import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES; -import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES; -import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_MULTI_ACCOUNTS; -import static eu.siacs.conversations.ui.SettingsActivity.INDICATE_RECEIVED; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_OWN_ACCOUNTS; -import static eu.siacs.conversations.utils.RichPreview.RICH_LINK_METADATA; - public class XmppConnectionService extends Service { public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations"; @@ -256,18 +253,9 @@ public class XmppConnectionService extends Service { Conversation conversation = find(getConversations(), contact); if (conversation != null) { if (online) { - conversation.endOtrIfNeeded(); if (contact.getPresences().size() == 1) { sendUnsentMessages(conversation); } - } else { - //check if the resource we are haveing a conversation with is still online - if (conversation.hasValidOtrSession()) { - String otrResource = conversation.getOtrSession().getSessionID().getUserID(); - if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) { - conversation.endOtrIfNeeded(); - } - } } } }; @@ -448,9 +436,6 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account && !pendingJoin && !inProgressJoin) { - if (!conversation.startOtrIfNeeded()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": couldn't start OTR with " + conversation.getContact().getJid() + " when needed"); - } sendUnsentMessages(conversation); } } @@ -505,7 +490,6 @@ public class XmppConnectionService extends Service { private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; private WakeLock wakeLock; - private PowerManager pm; private LruCache mBitmapCache; private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver(); private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver(); @@ -609,8 +593,8 @@ public class XmppConnectionService extends Service { return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize(); } - public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback callback) { - final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri); + public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback callback) { + final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type); final boolean compressPictures = getCompressImageResolutionPreference() != 0; if (!compressPictures || getFileBackend().useImageAsIs(uri) @@ -775,6 +759,7 @@ public class XmppConnectionService extends Service { } final CharSequence body = remoteInput.getCharSequence("text_reply"); final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false); + final String lastMessageUuid = intent.getStringExtra("last_message_uuid"); if (body == null || body.length() <= 0) { break; } @@ -783,15 +768,7 @@ public class XmppConnectionService extends Service { restoredFromDatabaseLatch.await(); final Conversation c = findConversationByUuid(uuid); if (c != null) { - boolean pn = false; - if (c.getMode() == Conversational.MODE_MULTI) { - final Message latestMessage = c.getLatestMessage(); - if (latestMessage.isPrivateMessage()) { - pn = true; - c.setNextCounterpart(latestMessage.getCounterpart()); - } - } - directReply(c, body.toString(), dismissNotification, pn); + directReply(c, body.toString(), lastMessageUuid, dismissNotification); } } catch (InterruptedException e) { Log.d(Config.LOGTAG, "unable to process direct reply"); @@ -860,9 +837,6 @@ public class XmppConnectionService extends Service { } } synchronized (this) { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M && Build.MANUFACTURER.equals("Huawei")) { - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "LocationManagerService"); - } WakeLockHelper.acquire(wakeLock); boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); final HashSet pingCandidates = new HashSet<>(); @@ -1040,10 +1014,11 @@ public class XmppConnectionService extends Service { } } - private void directReply(Conversation conversation, String body, final boolean dismissAfterReply, final boolean pn) { - Message message = new Message(conversation, body, conversation.getNextEncryption()); - if (pn) { - Message.configurePrivateMessage(message); + private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) { + final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid); + final Message message = new Message(conversation, body, conversation.getNextEncryption()); + if (inReplyTo != null && inReplyTo.isPrivateMessage()) { + Message.configurePrivateMessage(message, inReplyTo.getCounterpart()); } message.markUnread(); if (message.getEncryption() == Message.ENCRYPTION_PGP) { @@ -1173,6 +1148,23 @@ public class XmppConnectionService extends Service { } } + public DefaultAudioStrategy getCompressAudioPreference() { + switch (getPreferences().getString("video_compression", getResources().getString(R.string.video_compression))) { + case "verylow": + return TranscoderStrategies.AUDIO_LQ; + case "low": + return TranscoderStrategies.AUDIO_LQ; + case "mid": + return TranscoderStrategies.AUDIO_MQ; + case "high": + return TranscoderStrategies.AUDIO_HQ; + case "uncompressed": + return TranscoderStrategies.AUDIO_HQ; + default: + return TranscoderStrategies.AUDIO_MQ; + } + } + public boolean getAttachmentChoicePreference() { return getBooleanPreference(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, R.bool.quick_share_attachment_choice); } @@ -1412,6 +1404,7 @@ public class XmppConnectionService extends Service { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { startContactObserver(); } + FileBackend.switchStorage(usingInnerStorage()); FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath); if (Compatibility.hasStoragePermission(this)) { Log.d(Config.LOGTAG, "starting file observer"); @@ -1437,7 +1430,7 @@ public class XmppConnectionService extends Service { this.pgpServiceConnection.bindToService(); } - this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class); this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Config.LOGTAG + ":Service"); toggleForegroundService(); updateUnreadCountBadge(); @@ -1574,8 +1567,8 @@ public class XmppConnectionService extends Service { toggleForegroundService(false); } - public void setOngoingCall(AbstractJingleConnection.Id id, Set media) { - ongoingCall.set(new OngoingCall(id, media)); + public void setOngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { + ongoingCall.set(new OngoingCall(id, media, reconnecting)); toggleForegroundService(false); } @@ -1591,7 +1584,7 @@ public class XmppConnectionService extends Service { final Notification notification; final int id; if (ongoing != null) { - notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media); + notification = this.mNotificationService.getOngoingCallNotification(ongoing); id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; startForeground(id, notification); mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); @@ -1776,11 +1769,6 @@ public class XmppConnectionService extends Service { databaseBackend.updateConversation(conversation); } } - if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { - conversation.endOtrIfNeeded(); - conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, - message1 -> markMessage(message1, Message.STATUS_SEND_FAILED)); - } final boolean inProgressJoin = isJoinInProgress(conversation); @@ -1813,30 +1801,6 @@ public class XmppConnectionService extends Service { packet = mMessageGenerator.generatePgpChat(message); } break; - case Message.ENCRYPTION_OTR: - SessionImpl otrSession = conversation.getOtrSession(); - if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { - try { - message.setCounterpart(OtrJidHelper.fromSessionID(otrSession.getSessionID())); - } catch (IllegalArgumentException e) { - break; - } - if (message.needsUploading()) { - mJingleConnectionManager.startJingleFileTransfer(message); - } else { - packet = mMessageGenerator.generateOtrChat(message); - } - } else if (otrSession == null) { - if (message.fixCounterpart()) { - conversation.startOtrSession(message.getCounterpart().getResource(), true); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fix counterpart for OTR message to contact " + message.getCounterpart()); - break; - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " OTR session with " + message.getContact() + " is in wrong state: " + otrSession.getSessionStatus().toString()); - } - break; case Message.ENCRYPTION_AXOLOTL: message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); if (message.needsUploading()) { @@ -1889,12 +1853,6 @@ public class XmppConnectionService extends Service { } } break; - case Message.ENCRYPTION_OTR: - if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": create otr session without starting for " + message.getContact().getJid()); - conversation.startOtrSession(message.getCounterpart().getResource(), false); - } - break; case Message.ENCRYPTION_AXOLOTL: message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); break; @@ -2148,6 +2106,14 @@ public class XmppConnectionService extends Service { } } + public void pushBookmarks(Account account) { + if (account.getXmppConnection().getFeatures().bookmarksConversion()) { + pushBookmarksPep(account); + } else { + pushBookmarksPrivateXml(account); + } + } + private void pushBookmarksPrivateXml(Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); @@ -2166,7 +2132,6 @@ public class XmppConnectionService extends Service { storage.addChild(bookmark); } pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess()); - } private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options) { @@ -2219,7 +2184,10 @@ public class XmppConnectionService extends Service { long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore; Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms"); Runnable runnable = () -> { - long deletionDate = getAutomaticMessageDeletionDate(); + if (DatabaseBackend.requiresMessageIndexRebuild()) { + DatabaseBackend.getInstance(this).rebuildMessagesIndex(); + } + final long deletionDate = getAutomaticMessageDeletionDate(); mLastExpiryRun.set(SystemClock.elapsedRealtime()); if (deletionDate > 0) { Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate)); @@ -3728,35 +3696,26 @@ public class XmppConnectionService extends Service { public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { final Jid jid = user.asBareJid(); - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); - sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - conference.getMucOptions().changeAffiliation(jid, affiliation); - getAvatarService().clear(conference); + final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); + sendIqPacket(conference.getAccount(), request, (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + conference.getMucOptions().changeAffiliation(jid, affiliation); + getAvatarService().clear(conference); + if (callback != null) { callback.onAffiliationChangedSuccessful(jid); } else { - callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); + Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation); } + } else if (callback != null) { + callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); + } else { + Log.d(Config.LOGTAG, "unable to change affiliation"); } }); } - public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) { - List jids = new ArrayList<>(); - for (MucOptions.User user : conference.getMucOptions().getUsers()) { - if (user.getAffiliation() == before && user.getRealJid() != null) { - jids.add(user.getRealJid()); - } - } - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString()); - sendIqPacket(conference.getAccount(), request, mDefaultIqHandler); - } - public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) { IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - Log.d(Config.LOGTAG, request.toString()); sendIqPacket(conference.getAccount(), request, (account, packet) -> { if (packet.getType() != IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick); @@ -3798,12 +3757,6 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account) { if (conversation.getMode() == Conversation.MODE_MULTI) { leaveMuc(conversation, true); - } else { - if (conversation.endOtrIfNeeded()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": ended otr session with " - + conversation.getJid()); - } } } } @@ -3865,67 +3818,6 @@ public class XmppConnectionService extends Service { pushContactToServer(contact, null); } - - public void onOtrSessionEstablished(Conversation conversation) { - final Account account = conversation.getAccount(); - final Session otrSession = conversation.getOtrSession(); - Log.d(Config.LOGTAG, - account.getJid().asBareJid() + " otr session established with " - + conversation.getJid() + "/" - + otrSession.getSessionID().getUserID()); - conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { - - @Override - public void onMessageFound(Message message) { - SessionID id = otrSession.getSessionID(); - try { - message.setCounterpart(Jid.of(id.getAccountID() + "/" + id.getUserID())); - } catch (IllegalArgumentException e) { - return; - } - if (message.needsUploading()) { - mJingleConnectionManager.startJingleFileTransfer(message); - } else { - MessagePacket outPacket = mMessageGenerator.generateOtrChat(message); - if (outPacket != null) { - mMessageGenerator.addDelay(outPacket, message.getTimeSent()); - message.setStatus(Message.STATUS_SEND); - databaseBackend.updateMessage(message, false); - sendMessagePacket(account, outPacket); - } - } - updateConversationUi(); - } - }); - } - - public boolean renewSymmetricKey(Conversation conversation) { - Account account = conversation.getAccount(); - byte[] symmetricKey = new byte[32]; - this.mRandom.nextBytes(symmetricKey); - Session otrSession = conversation.getOtrSession(); - if (otrSession != null) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); - packet.setFrom(account.getJid()); - MessageGenerator.addMessageHints(packet); - packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/" - + otrSession.getSessionID().getUserID()); - try { - packet.setBody(otrSession - .transformSending(CryptoHelper.FILETRANSFER - + CryptoHelper.bytesToHex(symmetricKey))[0]); - sendMessagePacket(account, packet); - conversation.setSymmetricKey(symmetricKey); - return true; - } catch (OtrException e) { - return false; - } - } - return false; - } - - private void pushContactToServer(final Contact contact, final String preAuth) { contact.resetOption(Contact.Options.DIRTY_DELETE); contact.setOption(Contact.Options.DIRTY_PUSH); @@ -3939,10 +3831,10 @@ public class XmppConnectionService extends Service { iq.query(Namespace.ROSTER).addChild(contact.asElement()); account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); if (sendUpdates) { - sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth)); + sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); } if (ask) { - sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact)); + sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth)); } } else { syncRoster(contact.getAccount()); @@ -4403,9 +4295,13 @@ public class XmppConnectionService extends Service { new Thread(() -> reconnectAccount(account, false, true)).start(); } - public void invite(Conversation conversation, Jid contact) { + public void invite(final Conversation conversation, final Jid contact) { Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid()); - MessagePacket packet = mMessageGenerator.invite(conversation, contact); + final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid()); + if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) { + changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null); + } + final MessagePacket packet = mMessageGenerator.invite(conversation, contact); sendMessagePacket(conversation.getAccount(), packet); } @@ -4537,10 +4433,18 @@ public class XmppConnectionService extends Service { return getBooleanPreference(CONFIRM_MESSAGES, R.bool.confirm_messages); } + public boolean usingInnerStorage() { + return getBooleanPreference(USE_INNER_STORAGE, R.bool.use_inner_storage); + } + public boolean allowMessageCorrection() { return getBooleanPreference(ALLOW_MESSAGE_CORRECTION, R.bool.allow_message_correction); } + public boolean allowMessageRetraction() { + return getBooleanPreference(ALLOW_MESSAGE_RETRACTION, R.bool.allow_message_retraction); + } + public boolean sendChatStates() { return getBooleanPreference(CHAT_STATES, R.bool.chat_states); } @@ -4565,7 +4469,6 @@ public class XmppConnectionService extends Service { return getBooleanPreference(SettingsActivity.WARN_UNENCRYPTED_CHAT, R.bool.warn_unencrypted_chat); } - public boolean hideYouAreNotParticipating() { return getBooleanPreference(SettingsActivity.HIDE_YOU_ARE_NOT_PARTICIPATING, R.bool.hide_you_are_not_participating); } @@ -4586,6 +4489,14 @@ public class XmppConnectionService extends Service { return getBooleanPreference(SHOW_OWN_ACCOUNTS, R.bool.show_own_accounts); } + public boolean allowMergeMessages() { + return getBooleanPreference("allowmergemessages", R.bool.allowmergemessages); + } + + public boolean showTextFormatting() { + return getBooleanPreference("showtextformatting", R.bool.showtextformatting); + } + public int unreadCount() { int count = 0; for (Conversation conversation : getConversations()) { @@ -5252,10 +5163,7 @@ public class XmppConnectionService extends Service { boolean performedVerification = false; final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); for (XmppUri.Fingerprint fp : fingerprints) { - if (fp.type == XmppUri.FingerprintType.OTR) { - performedVerification |= contact.addOtrFingerprint(fp.fingerprint); - needsRosterWrite |= performedVerification; - } else if (fp.type == XmppUri.FingerprintType.OMEMO) { + if (fp.type == XmppUri.FingerprintType.OMEMO) { String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); if (fingerprintStatus != null) { @@ -5491,12 +5399,14 @@ public class XmppConnectionService extends Service { } public static class OngoingCall { - private final AbstractJingleConnection.Id id; - private final Set media; + public final AbstractJingleConnection.Id id; + public final Set media; + public final boolean reconnecting; - public OngoingCall(AbstractJingleConnection.Id id, Set media) { + public OngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { this.id = id; this.media = media; + this.reconnecting = reconnecting; } @Override @@ -5504,12 +5414,12 @@ public class XmppConnectionService extends Service { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OngoingCall that = (OngoingCall) o; - return Objects.equal(id, that.id); + return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media); } @Override public int hashCode() { - return Objects.hashCode(id); + return Objects.hashCode(id, media, reconnecting); } } diff --git a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java index e476a73a6..8480f98a5 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java @@ -37,7 +37,6 @@ public class AboutActivity extends XmppActivity { setContentView(R.layout.activity_about); setSupportActionBar(findViewById(R.id.toolbar)); configureActionBar(getSupportActionBar()); - setTitle(getString(R.string.title_activity_about_x, getString(R.string.app_name))); aboutmessage = findViewById(R.id.aboutmessage); libraries = findViewById(R.id.libraries); Button privacyButton = findViewById(R.id.show_privacy_policy); diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java index 7897ba5ab..82ffedcc3 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java @@ -5,23 +5,18 @@ import android.content.Intent; import android.preference.Preference; import android.util.AttributeSet; -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); - setSummaryAndTitle(context); + setSummary(); } 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)); - setTitle(context.getString(R.string.title_activity_about_x, BuildConfig.APP_NAME)); + setSummary(); } @Override @@ -30,5 +25,9 @@ public class AboutPreference extends Preference { final Intent intent = new Intent(getContext(), AboutActivity.class); getContext().startActivity(intent); } + + private void setSummary() { + setSummary(getContext().getString(R.string.app_name) + ' ' + PhoneHelper.getVersionName(getContext())); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index beef12fab..e39976ae5 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -4,10 +4,10 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue; import static eu.siacs.conversations.ui.util.IntroHelper.showIntro; import static eu.siacs.conversations.utils.StringUtils.changed; +import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.graphics.PorterDuff; import android.os.Bundle; import android.provider.Settings; import android.text.Editable; @@ -114,6 +114,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } }; + public static void open(final Activity activity, final Conversation conversation) { + Intent intent = new Intent(activity, ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", conversation.getUuid()); + activity.startActivity(intent); + activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + } + private OnClickListener mNotifyStatusClickListener = new OnClickListener() { @Override public void onClick(View v) { @@ -269,8 +277,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers if (mConversation != null) { final Bookmark bookmark = mConversation.getBookmark(); if (bookmark != null) { + this.binding.autojoinCheckbox.setEnabled(!getBooleanPreference("autojoin", R.bool.autojoin)); bookmark.setAutojoin(this.binding.autojoinCheckbox.isChecked()); - xmppConnectionService.createBookmark(mConversation.getAccount(), bookmark); + xmppConnectionService.pushBookmarks(mConversation.getAccount()); updateView(); } } @@ -330,8 +339,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.mAdvancedMode = !menuItem.isChecked(); menuItem.setChecked(this.mAdvancedMode); getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply(); - final boolean online = mConversation != null && mConversation.getMucOptions().online(); - this.binding.mucInfoMore.setVisibility(this.mAdvancedMode && online ? View.VISIBLE : View.GONE); invalidateOptionsMenu(); updateView(); break; @@ -483,7 +490,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers final MenuItem share = menu.findItem(R.id.action_share); share.setVisible(!groupChat); final MenuItem menuMessageNotification = menu.findItem(R.id.action_message_notifications); - if (Compatibility.runsTwentySix()) { + if (Compatibility.runsTwentySix() && xmppConnectionServiceBound) { menuMessageNotification.setVisible(xmppConnectionService.hasIndividualNotification(mConversation)); } else { menuMessageNotification.setVisible(false); @@ -598,7 +605,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } if (printableValue(subject)) { SpannableStringBuilder spannable = new SpannableStringBuilder(subject); - StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor()); + StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor(), true); MyLinkify.addLinks(spannable, false); this.binding.mucSubject.setText(EmojiWrapper.transform(spannable)); this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead); @@ -630,18 +637,15 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucInfoMam.setText(R.string.server_info_unavailable); } if (bookmark != null) { + this.binding.autojoinCheckbox.setEnabled(!getBooleanPreference("autojoin", R.bool.autojoin)); this.binding.autojoinCheckbox.setVisibility(View.VISIBLE); - if (bookmark.autojoin()) { - this.binding.autojoinCheckbox.setChecked(true); - } else { - this.binding.autojoinCheckbox.setChecked(false); - } + this.binding.autojoinCheckbox.setChecked(bookmark.autojoin()); } else { this.binding.autojoinCheckbox.setVisibility(View.GONE); } if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { if (mAdvancedMode) { - this.binding.destroy.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY); + this.binding.destroy.getBackground().setTint(getWarningButtonColor()); this.binding.destroy.setTextColor(getWarningTextColor()); this.binding.destroy.setVisibility(View.VISIBLE); } else { @@ -667,12 +671,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers }); LeaveMucDialog.create().show(); }); - this.binding.leaveMuc.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY); + this.binding.leaveMuc.getBackground().setTint(getWarningButtonColor()); this.binding.leaveMuc.setTextColor(getWarningTextColor()); this.binding.addContactButton.setVisibility(View.VISIBLE); if (mConversation.getBookmark() != null) { this.binding.addContactButton.setText(R.string.delete_bookmark); - this.binding.addContactButton.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY); + this.binding.addContactButton.getBackground().setTint(getWarningButtonColor()); this.binding.addContactButton.setTextColor(getWarningTextColor()); this.binding.addContactButton.setOnClickListener(v2 -> { final AlertDialog.Builder deleteFromRosterDialog = new AlertDialog.Builder(ConferenceDetailsActivity.this); @@ -682,6 +686,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers deleteFromRosterDialog.setPositiveButton(getString(R.string.delete), (dialog, which) -> { deleteBookmark(); + recreate(); }); deleteFromRosterDialog.create().show(); }); @@ -691,6 +696,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.addContactButton.setTextColor(getDefaultButtonTextColor()); this.binding.addContactButton.setOnClickListener(v2 -> { saveAsBookmark(); + recreate(); }); } } else { @@ -814,4 +820,4 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } } } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 341c93197..cfaa9d585 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.ui.util.IntroHelper.showIntro; + import android.Manifest; import android.content.ActivityNotFoundException; import android.content.Context; @@ -7,7 +9,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -28,8 +29,6 @@ import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageButton; import android.widget.TextView; -import eu.siacs.conversations.utils.CryptoHelper; - import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; @@ -84,8 +83,6 @@ import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import me.drakeet.support.toast.ToastCompat; -import static eu.siacs.conversations.ui.util.IntroHelper.showIntro; - public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded { public static final String ACTION_VIEW_CONTACT = "view_contact"; private final int REQUEST_SYNC_CONTACTS = 0x28cf; @@ -102,6 +99,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp @Override public void onClick(DialogInterface dialog, int which) { xmppConnectionService.deleteContactOnServer(contact); + recreate(); } }; private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() { @@ -161,6 +159,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } private void showAddToPhoneBookDialog() { + //TODO check if isQuicksy and contact is on quicksy.im domain + // store in final boolean. show different message. use phone number for add final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.action_add_phone_book)); builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toEscapedString())); @@ -170,6 +170,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp intent.setType(Contacts.CONTENT_ITEM_TYPE); intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toEscapedString()); intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER); + //TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP' + // however we don’t have such a field and thus have to use the legacy PROTOCOL_JABBER intent.putExtra("finishActivityOnSaveCompleted", true); try { startActivityForResult(intent, 0); @@ -319,6 +321,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) { @@ -553,7 +556,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp binding.detailsReceivePresence.setOnCheckedChangeListener(null); binding.addContactButton.setVisibility(View.VISIBLE); binding.addContactButton.setText(getString(R.string.action_delete_contact)); - binding.addContactButton.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY); + binding.addContactButton.getBackground().setTint(getWarningButtonColor()); binding.addContactButton.setTextColor(getWarningTextColor()); binding.addContactButton.setOnClickListener(view -> { final AlertDialog.Builder deleteFromRosterDialog = new AlertDialog.Builder(ContactDetailsActivity.this); @@ -677,26 +680,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp binding.detailsContactKeys.removeAllViews(); boolean hasKeys = false; LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - if (Config.supportOtr()) { - for (final String otrFingerprint : contact.getOtrFingerprints()) { - hasKeys = true; - View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false); - TextView key = view.findViewById(R.id.key); - TextView keyType = view.findViewById(R.id.key_type); - ImageButton removeButton = view - .findViewById(R.id.button_remove); - removeButton.setVisibility(View.VISIBLE); - key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); - if (otrFingerprint != null && otrFingerprint.equalsIgnoreCase(messageFingerprint)) { - keyType.setText(R.string.otr_fingerprint_selected_message); - keyType.setTextColor(ContextCompat.getColor(this, R.color.accent)); - } else { - keyType.setText(R.string.otr_fingerprint); - } - binding.detailsContactKeys.addView(view); - removeButton.setOnClickListener(v -> confirmToDeleteFingerprint(otrFingerprint)); - } - } final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); if (Config.supportOmemo() && axolotlService != null) { final Collection sessions = axolotlService.findSessionsForContact(contact); @@ -785,20 +768,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } } } - protected void confirmToDeleteFingerprint(final String fingerprint) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.delete_fingerprint); - builder.setMessage(R.string.sure_delete_fingerprint); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.delete, - (dialog, which) -> { - if (contact.deleteOtrFingerprint(fingerprint)) { - populateView(); - xmppConnectionService.syncRosterToDisk(contact.getAccount()); - } - }); - builder.create().show(); - } public void onBackendConnected() { if (accountJid != null && contactJid != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index a88399de7..1deba4c30 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1,5 +1,17 @@ package eu.siacs.conversations.ui; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static eu.siacs.conversations.ui.SettingsActivity.HIDE_YOU_ARE_NOT_PARTICIPATING; +import static eu.siacs.conversations.ui.SettingsActivity.WARN_UNENCRYPTED_CHAT; +import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; +import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; +import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; +import static eu.siacs.conversations.utils.PermissionUtils.readGranted; +import static eu.siacs.conversations.xmpp.Patches.ENCRYPTION_EXCEPTIONS; + import android.Manifest; import android.animation.Animator; import android.animation.AnimatorInflater; @@ -15,7 +27,6 @@ import android.content.IntentSender.SendIntentException; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Typeface; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -47,6 +58,7 @@ import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.CheckBox; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; @@ -56,18 +68,19 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.menu.MenuBuilder; import androidx.appcompat.view.menu.MenuPopupHelper; import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.databinding.DataBindingUtil; import com.google.common.base.Optional; +import org.jetbrains.annotations.NotNull; + +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -75,6 +88,8 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -90,6 +105,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Edit; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; @@ -104,16 +120,20 @@ import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.adapter.MediaPreviewAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter; +import eu.siacs.conversations.ui.adapter.MessageLogAdapter; +import eu.siacs.conversations.ui.adapter.model.MessageLogModel; import eu.siacs.conversations.ui.util.ActivityResult; import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.ui.util.CallManager; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.DateSeparator; import eu.siacs.conversations.ui.util.EditMessageActionModeCallback; +import eu.siacs.conversations.ui.util.KeyboardUtils; import eu.siacs.conversations.ui.util.ListViewUtils; import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.PresenceSelector; +import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.ui.util.ScrollState; import eu.siacs.conversations.ui.util.SendButtonAction; import eu.siacs.conversations.ui.util.SendButtonTool; @@ -125,6 +145,8 @@ import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MenuDoubleTabUtil; import eu.siacs.conversations.utils.MessageUtils; +import eu.siacs.conversations.utils.MimeUtils; +import eu.siacs.conversations.utils.Namespace; import eu.siacs.conversations.utils.NickValidityChecker; import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.QuickLoader; @@ -138,20 +160,6 @@ import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import me.drakeet.support.toast.ToastCompat; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.ui.SettingsActivity.HIDE_YOU_ARE_NOT_PARTICIPATING; -import static eu.siacs.conversations.ui.SettingsActivity.WARN_UNENCRYPTED_CHAT; -import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; -import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; -import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; -import static eu.siacs.conversations.utils.Compatibility.runsTwentyOne; -import static eu.siacs.conversations.utils.MessageUtils.fileWithKnownSize; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; -import static eu.siacs.conversations.xmpp.Patches.ENCRYPTION_EXCEPTIONS; -import net.java.otr4j.session.SessionStatus; - public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked { public static final int REQUEST_SEND_MESSAGE = 0x0201; @@ -200,23 +208,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private Toast messageLoaderToast; private ConversationsActivity activity; private Menu mOptionsMenu; - protected OnClickListener clickToVerify = new OnClickListener() { - @Override - public void onClick(View v) { - activity.verifyOtrSessionDialog(conversation, v); - } - }; + + private final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm (z)", Locale.US); + private boolean reInitRequiredOnStart = true; private MediaPreviewAdapter mediaPreviewAdapter; private final OnClickListener clickToMuc = new OnClickListener() { @Override public void onClick(View v) { - Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + ConferenceDetailsActivity.open(getActivity(), conversation); } }; private final OnClickListener leaveMuc = new OnClickListener() { @@ -258,6 +259,91 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke }); } }; + + private final OnClickListener meCommand = v -> Objects.requireNonNull(binding.textinput.getText()).insert(0, Message.ME_COMMAND + " "); + private final OnClickListener boldText = v -> insertFormatting("bold"); + private final OnClickListener italicText = v -> insertFormatting("italic"); + private final OnClickListener monospaceText = v -> insertFormatting("monospace"); + private final OnClickListener strikethroughText = v -> insertFormatting("strikethrough"); + private final OnClickListener help = v -> openHelp(); + private final OnClickListener close = v -> closeFormatting(); + + private void openHelp() { + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.format_text); + builder.setMessage(R.string.help_format_text); + builder.setNeutralButton(getString(R.string.ok), null); + builder.create().show(); + } + + private void closeFormatting() { + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.close); + builder.setMessage(R.string.close_format_text); + builder.setPositiveButton(getString(R.string.close), + (dialog, which) -> { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + preferences.edit().putBoolean("showtextformatting", false).apply(); + updateSendButton(); + }); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.create().show(); + } + + private void insertFormatting(String format) { + final String BOLD = "*"; + final String ITALIC = "_"; + final String MONOSPACE = "`"; + final String STRIKETHROUGH = "~"; + + int selStart = this.binding.textinput.getSelectionStart(); + int selEnd = this.binding.textinput.getSelectionEnd(); + int min = 0; + int max = this.binding.textinput.getText().length(); + if (this.binding.textinput.isFocused()) { + selStart = this.binding.textinput.getSelectionStart(); + selEnd = this.binding.textinput.getSelectionEnd(); + min = Math.max(0, Math.min(selStart, selEnd)); + max = Math.max(0, Math.max(selStart, selEnd)); + } + final CharSequence selectedText = this.binding.textinput.getText().subSequence(min, max); + + switch (format) { + case "bold": + if (selectedText.length() != 0) { + this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), + BOLD + selectedText + BOLD, 0, selectedText.length() + 2); + } else { + this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (BOLD)); + } + return; + case "italic": + if (selectedText.length() != 0) { + this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), + ITALIC + selectedText + ITALIC, 0, selectedText.length() + 2); + } else { + this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (ITALIC)); + } + return; + case "monospace": + if (selectedText.length() != 0) { + this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), + MONOSPACE + selectedText + MONOSPACE, 0, selectedText.length() + 2); + } else { + this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (MONOSPACE)); + } + return; + case "strikethrough": + if (selectedText.length() != 0) { + this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), + STRIKETHROUGH + selectedText + STRIKETHROUGH, 0, selectedText.length() + 2); + } else { + this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (STRIKETHROUGH)); + } + return; + } + } + private final OnScrollListener mOnScrollListener = new OnScrollListener() { @Override @@ -418,18 +504,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } }; - private OnClickListener mAnswerSmpClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = new Intent(activity, VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); - intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); - intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION); - startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }; protected OnClickListener clickToDecryptListener = new OnClickListener() { @@ -779,47 +853,19 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke }); } - private void attachPhotoToConversation(Conversation conversation, Uri uri) { - if (conversation == null) { - return; - } - final Toast prepareFileToast = ToastCompat.makeText(getActivity(), getText(R.string.preparing_image), ToastCompat.LENGTH_LONG); - prepareFileToast.show(); - activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachImageToConversation(conversation, uri, - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Message object) { - hidePrepareFileToast(prepareFileToast); - } - - @Override - public void success(Message message) { - hidePrepareFileToast(prepareFileToast); - } - - @Override - public void error(final int error, Message message) { - hidePrepareFileToast(prepareFileToast); - activity.runOnUiThread(() -> activity.replaceToast(getString(error))); - } - }); - } - public void attachEditorContentToConversation(Uri uri) { mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uri, Attachment.Type.FILE)); toggleInputMethod(); } - private void attachImageToConversation(Conversation conversation, Uri uri) { + private void attachImageToConversation(Conversation conversation, Uri uri, String type) { if (conversation == null) { return; } final Toast prepareFileToast = ToastCompat.makeText(getActivity(), getText(R.string.preparing_image), ToastCompat.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachImageToConversation(conversation, uri, + activity.xmppConnectionService.attachImageToConversation(conversation, uri, type, new UiCallback() { @Override public void userInputRequired(PendingIntent pi, Message object) { @@ -856,7 +902,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (body.length() == 0 || conversation == null) { return; } - if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(REQUEST_TRUST_KEYS_TEXT)) { + if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_TEXT)) { return; } final Message message; @@ -865,15 +911,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke Message.configurePrivateMessage(message); } else { message = conversation.getCorrectingMessage(); + message.putEdited(message.getUuid(), message.getServerMsgId(), message.getBody(), message.getTimeSent()); message.setBody(body); - message.putEdited(message.getUuid(), message.getServerMsgId()); message.setServerMsgId(null); message.setUuid(UUID.randomUUID().toString()); } switch (conversation.getNextEncryption()) { - case Message.ENCRYPTION_OTR: - sendOtrMessage(message); - break; case Message.ENCRYPTION_PGP: sendPgpMessage(message); break; @@ -882,6 +925,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } + private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) { + return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode); + } + protected boolean trustKeysIfNeeded(int requestCode) { AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); final List targets = axolotlService.getCryptoTargets(conversation); @@ -955,6 +1002,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case REQUEST_TRUST_KEYS_ATTACHMENTS: commitAttachments(); break; + case REQUEST_START_AUDIO_CALL: + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + break; + case REQUEST_START_VIDEO_CALL: + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); + break; case ATTACHMENT_CHOICE_CHOOSE_IMAGE: final List imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); mediaPreviewAdapter.addMediaPreviews(imageUris); @@ -980,9 +1033,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke toggleInputMethod(); break; case ATTACHMENT_CHOICE_LOCATION: - double latitude = data.getDoubleExtra("latitude", 0); - double longitude = data.getDoubleExtra("longitude", 0); - Uri geo = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude)); + final double latitude = data.getDoubleExtra("latitude", 0); + final double longitude = data.getDoubleExtra("longitude", 0); + final int accuracy = data.getIntExtra("accuracy", 0); + final Uri geo; + if (accuracy > 0) { + geo = Uri.parse(String.format("geo:%s,%s;u=%s", latitude, longitude, accuracy)); + } else { + geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude)); + } mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); toggleInputMethod(); break; @@ -1003,7 +1062,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { return; } - if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(REQUEST_TRUST_KEYS_ATTACHMENTS)) { + if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) { return; } final PresenceSelector.OnPresenceSelected callback = () -> { @@ -1013,7 +1072,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke attachLocationToConversation(conversation, attachment.getUri()); } else if (attachment.getType() == Attachment.Type.IMAGE) { Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); - attachImageToConversation(conversation, attachment.getUri()); + attachImageToConversation(conversation, attachment.getUri(), attachment.getMime()); } else { Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); attachFileToConversation(conversation, attachment.getUri(), attachment.getMime()); @@ -1047,11 +1106,19 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke binding.textinput.setVisibility(hasAttachments ? View.GONE : View.VISIBLE); binding.mediaPreview.setVisibility(hasAttachments ? View.VISIBLE : View.GONE); if (mOptionsMenu != null) { - ConversationMenuConfigurator.configureAttachmentMenu(conversation, mOptionsMenu, activity.xmppConnectionService.getAttachmentChoicePreference(), hasAttachments); + ConversationMenuConfigurator.configureAttachmentMenu(conversation, mOptionsMenu, activity.getAttachmentChoicePreference(), hasAttachments); } updateSendButton(); } + private boolean canSendMeCommand() { + if (conversation != null) { + final String body = binding.textinput.getText().toString(); + return body.length() == 0; + } + return false; + } + private void handleNegativeActivityResult(int requestCode) { switch (requestCode) { case ATTACHMENT_CHOICE_TAKE_PHOTO: @@ -1107,7 +1174,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke menuInflater.inflate(R.menu.fragment_conversation, menu); final MenuItem menuInviteContact = menu.findItem(R.id.action_invite); final MenuItem menuNeedHelp = menu.findItem(R.id.action_create_issue); - //final MenuItem menuSearchUpdates = menu.findItem(R.id.action_check_updates); + final MenuItem menuSearchUpdates = menu.findItem(R.id.action_check_updates); final MenuItem menuArchiveChat = menu.findItem(R.id.action_archive_chat); final MenuItem menuGroupDetails = menu.findItem(R.id.action_group_details); final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); @@ -1150,21 +1217,21 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke menuGroupDetails.setVisible(false); menuContactDetails.setVisible(!this.conversation.withSelf()); } - // menuSearchUpdates.setVisible(true); + menuSearchUpdates.setVisible(true); } else { menuGroupDetails.setVisible(false); menuContactDetails.setVisible(false); - //menuSearchUpdates.setVisible(false); + menuSearchUpdates.setVisible(false); } } catch (Exception e) { e.printStackTrace(); menuGroupDetails.setVisible(false); menuContactDetails.setVisible(false); - //menuSearchUpdates.setVisible(false); + menuSearchUpdates.setVisible(false); } menuMediaBrowser.setVisible(true); menuNeedHelp.setVisible(false); - ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, activity.xmppConnectionService.getAttachmentChoicePreference(), hasAttachments); + ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, activity.getAttachmentChoicePreference(), hasAttachments); ConversationMenuConfigurator.configureEncryptionMenu(conversation, menu, activity); if (conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false)) { menuTogglePinned.setTitle(R.string.remove_from_favorites); @@ -1173,7 +1240,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } else { menuNeedHelp.setVisible(true); - //menuSearchUpdates.setVisible(true); + menuSearchUpdates.setVisible(true); menuInviteContact.setVisible(false); menuGroupDetails.setVisible(false); menuContactDetails.setVisible(false); @@ -1221,12 +1288,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - int animator = enter ? R.animator.fade_right_in : R.animator.fade_right_out; - return AnimatorInflater.loadAnimator(getActivity(), animator); - } else { - return null; - } + int animator = enter ? R.animator.fade_right_in : R.animator.fade_right_out; + return AnimatorInflater.loadAnimator(this.activity, animator); + } private void quoteText(String text, String user) { @@ -1234,9 +1298,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke String username = ""; if (user != null && user.length() > 0) { if (user.equals(getString(R.string.me))) { - username = getString(R.string.i_have_written) + System.getProperty("line.separator"); + username = getString(R.string.me_quote) + System.getProperty("line.separator"); } else { - username = getString(R.string.x_has_written, user) + System.getProperty("line.separator"); + username = getString(R.string.x_user_quote, user) + System.getProperty("line.separator"); } } binding.textinput.insertAsQuote(username + text); @@ -1257,14 +1321,44 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else { binding.recordVoiceButton.setVisibility(View.GONE); } - Drawable unwrappedDrawable = AppCompatResources.getDrawable(activity.getBaseContext(), R.drawable.ic_send_voice_offline_white); - Drawable wrappedDrawable = DrawableCompat.wrap(unwrappedDrawable); - DrawableCompat.setTint(wrappedDrawable, StyledAttributes.getColor(activity, R.attr.colorAccent)); - binding.recordVoiceButton.setImageResource(R.drawable.ic_send_voice_offline_white); + binding.recordVoiceButton.setImageResource(activity.getThemeResource(R.attr.ic_send_voice_offline, R.drawable.ic_send_voice_offline)); + } + + private void quoteMedia(Message message, @Nullable String user) { + Message.FileParams params = message.getFileParams(); + String filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null; + final StringBuilder stringBuilder = new StringBuilder(); + if (activity.showDateInQuotes()) { + stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator")); + } + stringBuilder.append(MimeUtils.getMimeTypeEmoji(getActivity(), message.getMimeType())).append(" "); + stringBuilder.append(" \u00B7 "); + stringBuilder.append(filesize); + quoteText(stringBuilder.toString(), user); + } + + private void quoteGeoUri(Message message, @Nullable String user) { + final StringBuilder stringBuilder = new StringBuilder(); + if (activity.showDateInQuotes()) { + stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator")); + } + stringBuilder.append("\uD83D\uDDFA"); // map + quoteText(stringBuilder.toString(), user); } private void quoteMessage(Message message, @Nullable String user) { - quoteText(MessageUtils.prepareQuote(message), user); + if (message.isGeoUri()) { + quoteGeoUri(message, user); + } else if (message.isFileOrImage()) { + quoteMedia(message, user); + } else if (message.isTypeText()) { + final StringBuilder stringBuilder = new StringBuilder(); + if (activity.showDateInQuotes()) { + stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator")); + } + stringBuilder.append(MessageUtils.prepareQuote(message)); + quoteText(stringBuilder.toString(), user); + } } @Override @@ -1294,7 +1388,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (m.getStatus() == Message.STATUS_RECEIVED && t != null && (t.getStatus() == Transferable.STATUS_CANCELLED || t.getStatus() == Transferable.STATUS_FAILED)) { return; } - final boolean deleted = m.isFileDeleted(); + final boolean fileDeleted = m.isFileDeleted(); final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED || m.getEncryption() == Message.ENCRYPTION_PGP; final boolean receiving = m.getStatus() == Message.STATUS_RECEIVED && (t instanceof JingleFileTransferConnection || t instanceof HttpDownloadConnection); @@ -1313,14 +1407,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); MenuItem downloadFile = menu.findItem(R.id.download_file); MenuItem deleteFile = menu.findItem(R.id.delete_file); + MenuItem showLog = menu.findItem(R.id.show_edit_log); MenuItem showErrorMessage = menu.findItem(R.id.show_error_message); MenuItem saveFile = menu.findItem(R.id.save_file); final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(m); final boolean showError = m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); + final boolean messageDeleted = m.isMessageDeleted(); deleteMessage.setVisible(true); - if (!m.isFileOrImage() && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable() && !unInitiatedButKnownSize && t == null) { + if (!m.isFileOrImage() && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable() && !unInitiatedButKnownSize && t == null && !m.isMessageDeleted()) { copyMessage.setVisible(true); - quoteMessage.setVisible(!showError && MessageUtils.prepareQuote(m).length() > 0); String body = m.getMergedBody().toString(); if (ShareUtil.containsXmppUri(body)) { copyLink.setTitle(R.string.copy_jabber_id); @@ -1329,7 +1424,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke copyLink.setVisible(true); } } - if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !deleted) { + if (!encrypted && !unInitiatedButKnownSize && t == null) { + quoteMessage.setVisible(!showError && QuoteHelper.isMessageQuoteable(m)); + } + if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !fileDeleted) { retryDecryption.setVisible(true); } if (!showError @@ -1339,7 +1437,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke && m.getConversation() instanceof Conversation) { correctMessage.setVisible(true); } - if ((m.isFileOrImage() && !deleted && !receiving) || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) && !unInitiatedButKnownSize && t == null) { + if ((m.isFileOrImage() && !fileDeleted && !receiving) || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) && !unInitiatedButKnownSize && t == null && !messageDeleted) { shareWith.setVisible(true); } @@ -1354,21 +1452,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke || t instanceof HttpDownloadConnection) { copyUrl.setVisible(true); } - if (m.isFileOrImage() && deleted && m.hasFileOnRemoteHost()) { + if (m.isFileOrImage() && fileDeleted && m.hasFileOnRemoteHost()) { downloadFile.setVisible(true); downloadFile.setTitle(activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, m))); } final boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING || m.getStatus() == Message.STATUS_UNSEND || m.getStatus() == Message.STATUS_OFFERED; - final boolean cancelable = (t != null && !deleted) || waitingOfferedSending && m.needsUploading(); + final boolean cancelable = (t != null && !fileDeleted) || waitingOfferedSending && m.needsUploading(); if (cancelable) { cancelTransmission.setVisible(true); } - if (fileWithKnownSize(m)) { - cancelTransmission.setVisible(false); - } - if (m.isFileOrImage() && !deleted && !cancelable) { + if (m.isFileOrImage() && !fileDeleted && !cancelable) { String path = m.getRelativeFilePath(); Log.d(Config.LOGTAG, "Path = " + path); if (path == null || !path.startsWith("/") || path.contains(FileBackend.getConversationsDirectory("null"))) { @@ -1383,6 +1478,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(), m)) || (mime != null && mime.startsWith("audio/"))) { openWith.setVisible(true); } + if (m.edited() && m.getRetractId() == null) { + showLog.setVisible(true); + } } } @@ -1456,11 +1554,46 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case R.id.save_file: activity.xmppConnectionService.getFileBackend().saveFile(selectedMessage, activity); return true; + case R.id.show_edit_log: + openLog(selectedMessage); + return true; default: return super.onContextItemSelected(item); } } + private void openLog(Message logMsg) { + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.show_edit_log); + ArrayList dataModels = new ArrayList<>(); + for (Edit itm : logMsg.getEditedList()) { + dataModels.add(new MessageLogModel(itm.getBody(), itm.getTimeSent())); + } + dataModels.add(new MessageLogModel(logMsg.getBody(), logMsg.getTimeSent())); + + MessageLogAdapter adapter = new MessageLogAdapter(dataModels, getActivity()); + + LinearLayout layout = new LinearLayout(getActivity()); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setLayoutParams(layoutParams); + + ListView listView = new ListView(getActivity()); + listView.setLayoutParams(layoutParams); + layout.addView(listView); + + builder.setView(layout); + + listView.setAdapter(adapter); + + builder.setPositiveButton(R.string.ok, (dialog, which) -> { + dialog.dismiss(); + }); + final AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } + @Override public boolean onOptionsItemSelected(final MenuItem item) { Activity mXmppActivity = getActivity(); @@ -1471,7 +1604,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } switch (item.getItemId()) { case R.id.encryption_choice_axolotl: - case R.id.encryption_choice_otr: case R.id.encryption_choice_pgp: case R.id.encryption_choice_none: handleEncryptionSelection(item); @@ -1566,6 +1698,57 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke activity.invalidateOptionsMenu(); } + private void checkPermissionAndTriggerAudioCall() { + if (activity.mUseTor || conversation.getAccount().isOnion()) { + Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); + return; + } + if (hasPermissions(REQUEST_START_AUDIO_CALL, Manifest.permission.RECORD_AUDIO)) { + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + } + } + + private void checkPermissionAndTriggerVideoCall() { + if (activity.mUseTor || conversation.getAccount().isOnion()) { + Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); + return; + } + if (hasPermissions(REQUEST_START_VIDEO_CALL, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)) { + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); + } + } + + private void triggerRtpSession(final String action) { + if (activity.xmppConnectionService.getJingleConnectionManager().isBusy()) { + Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show(); + return; + } + final Contact contact = conversation.getContact(); + if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) { + triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action); + } else { + final RtpCapability.Capability capability; + if (action.equals(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL)) { + capability = RtpCapability.Capability.VIDEO; + } else { + capability = RtpCapability.Capability.AUDIO; + } + PresenceSelector.selectFullJidForDirectRtpConnection(activity, contact, capability, fullJid -> { + triggerRtpSession(contact.getAccount(), fullJid, action); + }); + } + } + + private void triggerRtpSession(final Account account, final Jid with, final String action) { + final Intent intent = new Intent(activity, RtpSessionActivity.class); + intent.setAction(action); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + private void handleAttachmentSelection(MenuItem item) { switch (item.getItemId()) { case R.id.attach_choose_picture: @@ -1602,10 +1785,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke updated = conversation.setNextEncryption(Message.ENCRYPTION_NONE); item.setChecked(true); break; - case R.id.encryption_choice_otr: - updated = conversation.setNextEncryption(Message.ENCRYPTION_OTR); - item.setChecked(true); - break; case R.id.encryption_choice_pgp: if (activity.hasPgp()) { if (conversation.getAccount().getPgpSignature() != null) { @@ -1772,6 +1951,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } if (readGranted(grantResults, permissions)) { if (activity != null && activity.xmppConnectionService != null) { + activity.xmppConnectionService.getBitmapCache().evictAll(); activity.xmppConnectionService.restartFileObserver(); } refresh(); @@ -1780,7 +1960,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void updateChatBG() { if (activity != null) { - if (activity.unicoloredBG() || !runsTwentyOne()) { + if (activity.unicoloredBG()) { binding.conversationsFragment.setBackgroundResource(0); binding.conversationsFragment.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_tertiary)); } else { @@ -1818,24 +1998,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true); } - private OnClickListener OTRwarning = new OnClickListener() { - @Override - public void onClick(View v) { - try { - final Uri uri = Uri.parse("https://github.com/kriztan/Pix-Art-Messenger/blob/master/docs/encryption.md"); - Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri); - startActivity(browserIntent); - } catch (Exception e) { - ToastCompat.makeText(activity, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show(); - } - } - }; @SuppressLint("InflateParams") protected void clearHistoryDialog(final Conversation conversation) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(getString(R.string.clear_conversation_history)); - final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); + final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox); if (conversation.getMode() == Conversation.MODE_SINGLE) { endConversationCheckBox.setVisibility(View.VISIBLE); @@ -1888,9 +2056,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke switch (attachmentChoice) { case ATTACHMENT_CHOICE_CHOOSE_IMAGE: intent.setAction(Intent.ACTION_GET_CONTENT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - } + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.setType("image/*"); chooser = true; break; @@ -1913,9 +2079,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case ATTACHMENT_CHOICE_CHOOSE_FILE: chooser = true; intent.setType("*/*"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - } + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setAction(Intent.ACTION_GET_CONTENT); break; @@ -2007,7 +2171,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void showErrorMessage(final Message message) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(R.string.error_message); final String errorMessage = message.getErrorMessage(); final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f"); @@ -2026,36 +2190,67 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke builder.create().show(); } - private void deleteMessage(Message message) { - Message relevantForCorrection = message; - while (message.mergeable(message.next())) { - message = message.next(); - } - while (relevantForCorrection.mergeable(relevantForCorrection.next())) { - relevantForCorrection = relevantForCorrection.next(); - } + private void deleteMessage(final Message message) { + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_message_dialog); builder.setMessage(R.string.delete_message_dialog_msg); - final Message finalRelevantForCorrection = relevantForCorrection; + final Message finalMessage = message; + builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (finalRelevantForCorrection.getType() == Message.TYPE_TEXT + + if (finalMessage.getType() == Message.TYPE_TEXT && !finalMessage.isGeoUri() - && finalRelevantForCorrection.isLastCorrectableMessage() - && finalMessage.getConversation() instanceof Conversation - && (((Conversation) finalMessage.getConversation()).getMucOptions().nonanonymous() || finalMessage.getConversation().getMode() == Conversation.MODE_SINGLE)) { - this.conversation.setCorrectingMessage(finalMessage); - Message deletedmessage = conversation.getCorrectingMessage(); - deletedmessage.setBody(DELETED_MESSAGE_BODY); - deletedmessage.putEdited(deletedmessage.getUuid(), deletedmessage.getServerMsgId()); - deletedmessage.setServerMsgId(null); - deletedmessage.setUuid(UUID.randomUUID().toString()); - sendMessage(deletedmessage); - activity.xmppConnectionService.deleteMessage(conversation, deletedmessage); + && finalMessage.getConversation() instanceof Conversation) { + + Message retractedMessage = finalMessage; + retractedMessage.setMessageDeleted(true); + + long time = System.currentTimeMillis(); + Message retractmessage = new Message(conversation, + "This person attempted to retract a previous message, but it's unsupported by your client.", + Message.ENCRYPTION_NONE, + Message.STATUS_SEND); + if (retractedMessage.getEditedList().size() > 0) { + retractmessage.setRetractId(retractedMessage.getEditedList().get(0).getEditedId()); + } else { + retractmessage.setRetractId(retractedMessage.getRemoteMsgId() != null ? retractedMessage.getRemoteMsgId() : retractedMessage.getUuid()); + } + + retractedMessage.putEdited(retractedMessage.getUuid(), retractedMessage.getServerMsgId(), retractedMessage.getBody(), retractedMessage.getTimeSent()); + retractedMessage.setBody(Message.DELETED_MESSAGE_BODY); + retractedMessage.setServerMsgId(null); + retractedMessage.setRemoteMsgId(message.getRemoteMsgId()); + retractedMessage.setMessageDeleted(true); + + retractmessage.setType(Message.TYPE_TEXT); + retractmessage.setCounterpart(message.getCounterpart()); + retractmessage.setTrueCounterpart(message.getTrueCounterpart()); + retractmessage.setTime(time); + retractmessage.setUuid(UUID.randomUUID().toString()); + retractmessage.setCarbon(false); + retractmessage.setOob(false); + retractmessage.setRemoteMsgId(retractmessage.getUuid()); + retractmessage.setMessageDeleted(true); + retractedMessage.setTime(time); //set new time here to keep orginal timestamps + for (Edit itm : retractedMessage.getEditedList()) { + Message tmpRetractedMessage = conversation.findMessageWithUuidOrRemoteId(itm.getEditedId()); + if (tmpRetractedMessage != null) { + tmpRetractedMessage.setMessageDeleted(true); + activity.xmppConnectionService.updateMessage(tmpRetractedMessage, tmpRetractedMessage.getUuid()); + } + } + activity.xmppConnectionService.updateMessage(retractedMessage, retractedMessage.getUuid()); + if (finalMessage.getStatus() >= Message.STATUS_SEND) { + //only send retraction messages vor outgoing messages! + sendMessage(retractmessage); + } + activity.xmppConnectionService.deleteMessage(conversation, retractedMessage); + activity.xmppConnectionService.deleteMessage(conversation, retractmessage); } - activity.xmppConnectionService.deleteMessage(conversation, finalMessage); + activity.xmppConnectionService.deleteMessage(conversation, message); activity.onConversationsListItemUpdated(); refresh(); }); @@ -2063,7 +2258,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void deleteFile(final Message message) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_file_dialog); builder.setMessage(R.string.delete_file_dialog_msg); @@ -2143,7 +2338,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } - private void cancelTransmission(Message message) { + public void cancelTransmission(Message message) { Transferable transferable = message.getTransferable(); if (transferable != null) { transferable.cancel(); @@ -2160,19 +2355,25 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } public void privateMessageWith(final Jid counterpart) { - final Jid tcp = conversation.getMucOptions().getTrueCounterpart(counterpart); - if (!getConversation().getMucOptions().isUserInRoom(counterpart) && getConversation().getMucOptions().findUserByRealJid(tcp == null ? null : tcp.asBareJid()) == null) { - ToastCompat.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, counterpart.getResource()), ToastCompat.LENGTH_SHORT).show(); - return; + try { + final Jid tcp = conversation.getMucOptions().getTrueCounterpart(counterpart); + if (!getConversation().getMucOptions().isUserInRoom(counterpart) && getConversation().getMucOptions().findUserByRealJid(tcp == null ? null : tcp.asBareJid()) == null) { + ToastCompat.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, counterpart.getResource()), ToastCompat.LENGTH_SHORT).show(); + return; + } + if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { + activity.xmppConnectionService.sendChatState(conversation); + } + this.binding.textinput.setText(""); + this.conversation.setNextCounterpart(counterpart); + } catch (Exception e) { + e.printStackTrace(); + ToastCompat.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, activity.getString(R.string.user)), ToastCompat.LENGTH_SHORT).show(); + } finally { + updateChatMsgHint(); + updateSendButton(); + updateEditablity(); } - if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { - activity.xmppConnectionService.sendChatState(conversation); - } - this.binding.textinput.setText(""); - this.conversation.setNextCounterpart(counterpart); - updateChatMsgHint(); - updateSendButton(); - updateEditablity(); } private void correctMessage(Message message) { @@ -2223,7 +2424,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NotNull Bundle outState) { super.onSaveInstanceState(outState); if (conversation != null) { outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid()); @@ -2476,13 +2677,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final String user = extras.getString(ConversationsActivity.EXTRA_USER); final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false); final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); + final String type = extras.getString(ConversationsActivity.EXTRA_TYPE); final List uris = extractUris(extras); if (uris != null && uris.size() > 0) { if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) { mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); } else { final List cleanedUris = cleanUris(new ArrayList<>(uris)); - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris)); + mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type)); } toggleInputMethod(); return; @@ -2650,24 +2852,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } else if (account.hasPendingPgpIntent(conversation)) { showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener); - } else if (mode == Conversation.MODE_SINGLE - && conversation.smpRequested()) { - showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener); - } else if (mode == Conversation.MODE_SINGLE - && conversation.hasValidOtrSession() - && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) - && (!conversation.isOtrFingerprintVerified())) { - showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); - } else if (connection != null - && connection.getFeatures().blocking() - && conversation.countMessages() != 0 - && !conversation.isBlocked() - && conversation.isWithStranger()) { - showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener); - } else if (activity.xmppConnectionService.warnUnecryptedChat()) { + } else if (activity.warnUnecryptedChat()) { if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE && conversation.isSingleOrPrivateAndNonAnonymous() && ((Config.supportOmemo() && Conversation.suitableForOmemoByDefault(conversation)) || - (Config.supportOpenPgp() && account.isPgpDecryptionServiceConnected()) || ( - mode == Conversation.MODE_SINGLE && Config.supportOtr()))) { + (Config.supportOpenPgp() && account.isPgpDecryptionServiceConnected()))) { if (ENCRYPTION_EXCEPTIONS.contains(conversation.getJid().toString()) || conversation.getJid().toString().equals(account.getJid().getDomain())) { hideSnackbar(); } else { @@ -2785,6 +2972,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize(); } + private void updateTextFormat(final boolean me) { + KeyboardUtils.addKeyboardToggleListener(activity, isVisible -> { + Log.d(Config.LOGTAG, "keyboard visible: " + isVisible); + if (isVisible && activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.showTextFormatting()) { + showTextFormat(me); + } else { + hideTextFormat(); + } + }); + } + private void updateEditablity() { boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null; this.binding.textinput.setFocusable(canWrite); @@ -2825,6 +3023,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } updateSnackBar(conversation); updateChatMsgHint(); + updateTextFormat(canSendMeCommand()); } protected void updateStatusMessages() { @@ -3046,16 +3245,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke builder.setPositiveButton(getString(R.string.send_unencrypted), listener); builder.create().show(); } - protected void sendOtrMessage(final Message message) { - final ConversationsActivity activity = (ConversationsActivity) getActivity(); - final XmppConnectionService xmppService = activity.xmppConnectionService; - activity.selectPresence(conversation, - () -> { - message.setCounterpart(conversation.getNextCounterpart()); - xmppService.sendMessage(message); - messageSent(); - }); - } public void appendText(String text, final boolean doNotAppend) { if (text == null) { @@ -3111,6 +3300,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke service.sendChatState(conversation); } runOnUiThread(this::updateSendButton); + } @Override @@ -3311,7 +3501,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke popupMenu.setOnMenuItemClickListener(item -> { final XmppActivity activity = this.activity; if (activity == null) { - Log.e(Config.LOGTAG,"Unable to perform action. no context provided"); + Log.e(Config.LOGTAG, "Unable to perform action. no context provided"); return true; } switch (item.getItemId()) { @@ -3393,4 +3583,38 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } activity.switchToAccount(message.getConversation().getAccount(), fingerprint); } + + private Activity requireActivity() { + final Activity activity = getActivity(); + if (activity == null) { + throw new IllegalStateException("Activity not attached"); + } + return activity; + } + + private void showTextFormat(final boolean me) { + this.binding.textformat.setVisibility(View.VISIBLE); + this.binding.me.setEnabled(me); + this.binding.me.setOnClickListener(meCommand); + this.binding.bold.setOnClickListener(boldText); + this.binding.italic.setOnClickListener(italicText); + this.binding.monospace.setOnClickListener(monospaceText); + this.binding.strikethrough.setOnClickListener(strikethroughText); + this.binding.help.setOnClickListener(help); + this.binding.close.setOnClickListener(close); + if (Compatibility.runsTwentyEight()) { + this.binding.me.setTooltipText(activity.getString(R.string.me)); + this.binding.bold.setTooltipText(activity.getString(R.string.bold)); + this.binding.italic.setTooltipText(activity.getString(R.string.italic)); + this.binding.monospace.setTooltipText(activity.getString(R.string.monospace)); + this.binding.monospace.setTooltipText(activity.getString(R.string.monospace)); + this.binding.strikethrough.setTooltipText(activity.getString(R.string.strikethrough)); + this.binding.help.setTooltipText(activity.getString(R.string.help)); + this.binding.close.setTooltipText(activity.getString(R.string.close)); + } + } + + private void hideTextFormat() { + this.binding.textformat.setVisibility(View.GONE); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 87ad3449e..400f6f7de 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -29,7 +29,10 @@ package eu.siacs.conversations.ui; -import net.java.otr4j.session.SessionStatus; +import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP; +import static eu.siacs.conversations.ui.SettingsActivity.HIDE_MEMORY_WARNING; +import static eu.siacs.conversations.ui.SettingsActivity.MIN_ANDROID_SDK21_SHOWN; + import android.annotation.SuppressLint; import android.app.Activity; import android.app.Fragment; @@ -53,9 +56,7 @@ import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.PopupMenu; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.IdRes; import androidx.annotation.NonNull; @@ -77,6 +78,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.OmemoSetting; import eu.siacs.conversations.databinding.ActivityConversationsBinding; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.MucOptions; @@ -87,13 +89,13 @@ import eu.siacs.conversations.ui.interfaces.OnConversationArchived; import eu.siacs.conversations.ui.interfaces.OnConversationRead; import eu.siacs.conversations.ui.interfaces.OnConversationSelected; import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated; +import eu.siacs.conversations.ui.util.ActionBarUtil; import eu.siacs.conversations.ui.util.ActivityResult; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.IntroHelper; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.ui.util.UpdateHelper; -import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.MenuDoubleTabUtil; @@ -106,10 +108,6 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.chatstate.ChatState; import me.drakeet.support.toast.ToastCompat; -import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP; -import static eu.siacs.conversations.ui.SettingsActivity.HIDE_MEMORY_WARNING; -import static eu.siacs.conversations.ui.SettingsActivity.MIN_ANDROID_SDK21_SHOWN; - public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoomDestroy { public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.VIEW"; @@ -125,7 +123,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public static final String ACTION_DESTROY_MUC = "eu.siacs.conversations.DESTROY_MUC"; public static final int REQUEST_OPEN_MESSAGE = 0x9876; public static final int REQUEST_PLAY_PAUSE = 0x5432; - private static List VIEW_AND_SHARE_ACTIONS = Arrays.asList( + public static final String EXTRA_TYPE = "type"; + + private static final List VIEW_AND_SHARE_ACTIONS = Arrays.asList( ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE @@ -215,6 +215,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio System.exit(0); } + if (useInternalUpdater()) { + if (xmppConnectionService.getAccounts().size() != 0) { + if (xmppConnectionService.hasInternetConnection()) { + if (xmppConnectionService.isWIFI() || (xmppConnectionService.isMobile() && !xmppConnectionService.isMobileRoaming())) { + AppUpdate(xmppConnectionService.installedFrom()); + } + } + } + } + for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) { notifyFragmentOfBackendConnected(id); } @@ -276,7 +286,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private void showOutdatedVersionWarning() { - if (Compatibility.runsTwentyOne() || getPreferences().getBoolean(MIN_ANDROID_SDK21_SHOWN, false)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP || getPreferences().getBoolean(MIN_ANDROID_SDK21_SHOWN, false)) { Log.d(Config.LOGTAG, "Device is running Android >= SDK 21"); return; } @@ -302,6 +312,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply(); } + public boolean getAttachmentChoicePreference() { + return getBooleanPreference(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, R.bool.quick_share_attachment_choice); + } + + public boolean warnUnecryptedChat() { + return getBooleanPreference(SettingsActivity.WARN_UNENCRYPTED_CHAT, R.bool.warn_unencrypted_chat); + } + private void openBatteryOptimizationDialogIfNeeded() { if (hasAccountWithoutPush() && isOptimizingBattery() @@ -320,9 +338,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio ToastCompat.makeText(this, R.string.device_does_not_support_battery_op, ToastCompat.LENGTH_SHORT).show(); } }); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain()); - } + builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain()); final AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); dialog.show(); @@ -349,12 +365,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio @Override protected Void doInBackground(Void... params) { - totalMemory = FileBackend.getDiskSize(); - mediaUsage = FileBackend.getDirectorySize(new File(FileBackend.getAppMediaDirectory())); try { + totalMemory = FileBackend.getDiskSize(); + mediaUsage = FileBackend.getDirectorySize(new File(FileBackend.getAppMediaDirectory())); relativeUsage = ((double) mediaUsage / (double) totalMemory); try { - percentUsage = String.format("%.2f", relativeUsage * 100) + " %"; + percentUsage = String.format(Locale.getDefault(),"%.2f", relativeUsage * 100) + " %"; } catch (Exception e) { e.printStackTrace(); percentUsage = String.format(Locale.ENGLISH,"%.2f", relativeUsage * 100) + " %"; @@ -611,8 +627,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private void openConversation(Conversation conversation, Bundle extras) { + final FragmentManager fragmentManager = getFragmentManager(); + executePendingTransactions(fragmentManager); + ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment); xmppConnectionService.updateNotificationChannels(); - ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment); final boolean mainNeedsRefresh; if (conversationFragment == null) { mainNeedsRefresh = false; @@ -648,6 +666,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio IntroHelper.showIntro(this, conversation.getMode() == Conversational.MODE_MULTI); } + private static void executePendingTransactions(final FragmentManager fragmentManager) { + try { + fragmentManager.executePendingTransactions(); + } catch (final Exception e) { + Log.e(Config.LOGTAG,"unable to execute pending fragment transactions"); + } + } + public boolean onXmppUriClicked(Uri uri) { XmppUri xmppUri = new XmppUri(uri); if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) { @@ -660,6 +686,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio return false; } + @SuppressLint("NonConstantResourceId") @Override public boolean onOptionsItemSelected(MenuItem item) { if (MenuDoubleTabUtil.shouldIgnoreTap()) { @@ -692,13 +719,13 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid()); startActivity(intent); return true; - //case R.id.action_check_updates: - // if (xmppConnectionService.hasInternetConnection()) { - // openInstallFromUnknownSourcesDialogIfNeeded(true); - // } else { - // ToastCompat.makeText(this, R.string.account_status_no_internet, ToastCompat.LENGTH_LONG).show(); - // } - // break; + case R.id.action_check_updates: + if (xmppConnectionService.hasInternetConnection()) { + openInstallFromUnknownSourcesDialogIfNeeded(true); + } else { + ToastCompat.makeText(this, R.string.account_status_no_internet, ToastCompat.LENGTH_LONG).show(); + } + break; case R.id.action_invite_user: inviteUser(); break; @@ -726,6 +753,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio @Override protected void onStart() { + super.onStart(); final int theme = findTheme(); if (this.mTheme != theme) { this.mSkipBackgroundBinding = true; @@ -835,164 +863,113 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private void invalidateActionBarTitle() { final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); - if (mainFragment instanceof ConversationFragment) { - final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); - if (conversation != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - final View view = getLayoutInflater().inflate(R.layout.ab_title, null); - getSupportActionBar().setCustomView(view); - actionBar.setIcon(null); - actionBar.setBackgroundDrawable(new ColorDrawable(StyledAttributes.getColor(this, R.attr.colorPrimary))); - actionBar.setDisplayShowTitleEnabled(false); - actionBar.setDisplayShowCustomEnabled(true); - TextView abtitle = findViewById(android.R.id.text1); - TextView absubtitle = findViewById(android.R.id.text2); - abtitle.setText(EmojiWrapper.transform(conversation.getName())); - abtitle.setOnClickListener(view1 -> { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - switchToContactDetails(conversation.getContact()); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }); - abtitle.setSelected(true); - if (conversation.getMode() == Conversation.MODE_SINGLE && !conversation.withSelf()) { + if (actionBar == null) { + return; + } + final FragmentManager fragmentManager = getFragmentManager(); + final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); + if (mainFragment instanceof ConversationFragment) { + final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); + if (conversation != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + final View view = getLayoutInflater().inflate(R.layout.ab_title, null); + getSupportActionBar().setCustomView(view); + actionBar.setIcon(null); + actionBar.setBackgroundDrawable(new ColorDrawable(StyledAttributes.getColor(this, R.attr.colorPrimary))); + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setDisplayShowCustomEnabled(true); + TextView abtitle = findViewById(android.R.id.text1); + TextView absubtitle = findViewById(android.R.id.text2); + abtitle.setText(EmojiWrapper.transform(conversation.getName())); + abtitle.setSelected(true); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (!conversation.withSelf()) { ChatState state = conversation.getIncomingChatState(); - if (state == ChatState.COMPOSING) { - absubtitle.setText(getString(R.string.is_typing)); - absubtitle.setVisibility(View.VISIBLE); - absubtitle.setTypeface(null, Typeface.BOLD_ITALIC); - absubtitle.setSelected(true); - absubtitle.setOnClickListener(view13 -> { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - switchToContactDetails(conversation.getContact()); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }); - } else { - if (showLastSeen && conversation.getContact().getLastseen() > 0 && conversation.getContact().getPresences().allOrNonSupport(Namespace.IDLE)) { - absubtitle.setText(UIHelper.lastseen(getApplicationContext(), conversation.getContact().isActive(), conversation.getContact().getLastseen())); - absubtitle.setVisibility(View.VISIBLE); - } else { - absubtitle.setText(null); - absubtitle.setVisibility(View.GONE); - } - absubtitle.setSelected(true); - absubtitle.setOnClickListener(view14 -> { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - switchToContactDetails(conversation.getContact()); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }); - } - } else { - ChatState state = ChatState.COMPOSING; - List userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); - if (userWithChatStates.size() == 0) { - state = ChatState.PAUSED; - userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); - } - List users = conversation.getMucOptions().getUsers(true); if (state == ChatState.COMPOSING) { - if (userWithChatStates.size() > 0) { - if (userWithChatStates.size() == 1) { - MucOptions.User user = userWithChatStates.get(0); - absubtitle.setText(EmojiWrapper.transform(getString(R.string.contact_is_typing, UIHelper.getDisplayName(user)))); - absubtitle.setVisibility(View.VISIBLE); - } else { - StringBuilder builder = new StringBuilder(); - for (MucOptions.User user : userWithChatStates) { - if (builder.length() != 0) { - builder.append(", "); - } - builder.append(UIHelper.getDisplayName(user)); - } - absubtitle.setText(EmojiWrapper.transform(getString(R.string.contacts_are_typing, builder.toString()))); - absubtitle.setVisibility(View.VISIBLE); - } - } + absubtitle.setText(getString(R.string.is_typing)); + absubtitle.setVisibility(View.VISIBLE); + absubtitle.setTypeface(null, Typeface.BOLD_ITALIC); + absubtitle.setSelected(true); } else { - if (users.size() == 0) { - absubtitle.setText(getString(R.string.one_participant)); + if (showLastSeen && conversation.getContact().getLastseen() > 0 && conversation.getContact().getPresences().allOrNonSupport(Namespace.IDLE)) { + absubtitle.setText(UIHelper.lastseen(getApplicationContext(), conversation.getContact().isActive(), conversation.getContact().getLastseen())); absubtitle.setVisibility(View.VISIBLE); } else { - int size = users.size() + 1; - absubtitle.setText(getString(R.string.more_participants, size)); + absubtitle.setText(null); + absubtitle.setVisibility(View.GONE); + } + absubtitle.setSelected(true); + } + } else { + absubtitle.setText(null); + absubtitle.setVisibility(View.GONE); + } + } else { + ChatState state = ChatState.COMPOSING; + List userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); + if (userWithChatStates.size() == 0) { + state = ChatState.PAUSED; + userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); + } + List users = conversation.getMucOptions().getUsers(true); + if (state == ChatState.COMPOSING) { + if (userWithChatStates.size() > 0) { + if (userWithChatStates.size() == 1) { + MucOptions.User user = userWithChatStates.get(0); + absubtitle.setText(EmojiWrapper.transform(getString(R.string.contact_is_typing, UIHelper.getDisplayName(user)))); + absubtitle.setVisibility(View.VISIBLE); + } else { + StringBuilder builder = new StringBuilder(); + for (MucOptions.User user : userWithChatStates) { + if (builder.length() != 0) { + builder.append(", "); + } + builder.append(UIHelper.getDisplayName(user)); + } + absubtitle.setText(EmojiWrapper.transform(getString(R.string.contacts_are_typing, builder.toString()))); absubtitle.setVisibility(View.VISIBLE); } } - absubtitle.setSelected(true); - absubtitle.setOnClickListener(view15 -> { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - switchToContactDetails(conversation.getContact()); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }); + } else { + if (users.size() == 0) { + absubtitle.setText(getString(R.string.one_participant)); + absubtitle.setVisibility(View.VISIBLE); + } else { + int size = users.size() + 1; + absubtitle.setText(getString(R.string.more_participants, size)); + absubtitle.setVisibility(View.VISIBLE); + } } - return; + absubtitle.setSelected(true); } + ActionBarUtil.setCustomActionBarOnClickListener( + binding.toolbar, + (v) -> openConversationDetails(conversation) + ); + return; } - actionBar.setDisplayShowTitleEnabled(true); - actionBar.setDisplayShowCustomEnabled(false); - actionBar.setTitle(null); - actionBar.setIcon(R.drawable.logo_actionbar); - actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.header_background))); - actionBar.setSubtitle(null); - actionBar.setDisplayHomeAsUpEnabled(false); } + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayShowCustomEnabled(false); + actionBar.setTitle(null); + actionBar.setIcon(R.drawable.logo_actionbar); + actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.header_background))); + actionBar.setSubtitle(null); + actionBar.setDisplayHomeAsUpEnabled(false); + ActionBarUtil.resetCustomActionBarOnClickListeners(binding.toolbar); } - public void verifyOtrSessionDialog(final Conversation conversation, View view) { - if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { - ToastCompat.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show(); - return; - } - if (view == null) { - return; - } - PopupMenu popup = new PopupMenu(this, view); - popup.inflate(R.menu.verification_choices); - popup.setOnMenuItemClickListener(menuItem -> { - Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString()); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); - switch (menuItem.getItemId()) { - case R.id.scan_fingerprint: - intent.putExtra("mode", VerifyOTRActivity.MODE_SCAN_FINGERPRINT); - break; - case R.id.ask_question: - intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION); - break; - case R.id.manual_verification: - intent.putExtra("mode", VerifyOTRActivity.MODE_MANUAL_VERIFICATION); - break; + + private void openConversationDetails(final Conversation conversation) { + if (conversation.getMode() == Conversational.MODE_MULTI) { + ConferenceDetailsActivity.open(this, conversation); + } else { + final Contact contact = conversation.getContact(); + if (contact.isSelf()) { + switchToAccount(conversation.getAccount()); + } else { + switchToContactDetails(contact); } - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - return true; - }); - popup.show(); + } } @Override @@ -1000,17 +977,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (performRedirectIfNecessary(conversation, false)) { return; } - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); + final FragmentManager fragmentManager = getFragmentManager(); + final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); if (mainFragment instanceof ConversationFragment) { try { - getFragmentManager().popBackStack(); - } catch (IllegalStateException e) { + fragmentManager.popBackStack(); + } catch (final IllegalStateException e) { Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e); //this usually means activity is no longer active; meaning on the next open we will run through this again } return; } - Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment); + final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment); if (secondaryFragment instanceof ConversationFragment) { if (((ConversationFragment) secondaryFragment).getConversation() == conversation) { Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation); @@ -1072,6 +1050,25 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio runOnUiThread(() -> ToastCompat.makeText(this, resId, ToastCompat.LENGTH_SHORT).show()); } + protected void AppUpdate(String Store) { + String PREFS_NAME = "UpdateTimeStamp"; + SharedPreferences UpdateTimeStamp = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + long lastUpdateTime = UpdateTimeStamp.getLong("lastUpdateTime", 0); + Log.d(Config.LOGTAG, "AppUpdater: LastUpdateTime: " + lastUpdateTime); + if ((lastUpdateTime + (Config.UPDATE_CHECK_TIMER * 1000)) < System.currentTimeMillis()) { + lastUpdateTime = System.currentTimeMillis(); + SharedPreferences.Editor editor = UpdateTimeStamp.edit(); + editor.putLong("lastUpdateTime", lastUpdateTime); + editor.apply(); + Log.d(Config.LOGTAG, "AppUpdater: CurrentTime: " + lastUpdateTime); + if (Store == null) { + Log.d(Config.LOGTAG, "AppUpdater started"); + openInstallFromUnknownSourcesDialogIfNeeded(false); + } + } else { + Log.d(Config.LOGTAG, "AppUpdater stopped"); + } + } @Override public void onRoomDestroySucceeded() { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index 0a45294e2..731c21b0a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -121,20 +121,20 @@ public class ConversationsOverviewFragment extends XmppFragment { } } - @Override - public void onDestroyView() { - Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroyView()"); - super.onDestroyView(); - this.binding = null; - this.conversationsAdapter = null; - } + @Override + public void onDestroyView() { + Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroyView()"); + super.onDestroyView(); + this.binding = null; + this.conversationsAdapter = null; + } - @Override - public void onDestroy() { - Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroy()"); - super.onDestroy(); + @Override + public void onDestroy() { + Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroy()"); + super.onDestroy(); - } + } @Override public void onPause() { @@ -236,7 +236,7 @@ public class ConversationsOverviewFragment extends XmppFragment { return true; } return super.onOptionsItemSelected(item); - } + } @Override public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { diff --git a/src/main/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java b/src/main/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java index 2cf8e049f..2a2987aef 100644 --- a/src/main/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java @@ -37,7 +37,6 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite); setSupportActionBar((Toolbar) binding.toolbar); configureActionBar(getSupportActionBar(), true); - this.binding.shareButton.setOnClickListener(v -> share()); if (bundle != null && bundle.containsKey("invite")) { this.easyOnboardingInvite = bundle.getParcelable("invite"); if (this.easyOnboardingInvite != null) { @@ -100,7 +99,7 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn 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 Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableUri(), width); binding.qrCode.setImageBitmap(bitmap); } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 523a401fb..fe4ed0fb1 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.readGranted; + import android.app.Activity; import android.app.PendingIntent; import android.content.ActivityNotFoundException; @@ -33,7 +36,6 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; import com.google.android.material.textfield.TextInputLayout; @@ -86,9 +88,6 @@ import eu.siacs.conversations.xmpp.pep.Avatar; import me.drakeet.support.toast.ToastCompat; import okhttp3.HttpUrl; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; - public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched { @@ -428,6 +427,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (view.getId() == R.id.hostname) { resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example; } + if (view.getId() == R.id.port) { + resId = R.string.port_example; + } final int res = resId; new Handler().postDelayed(() -> et.setHint(res), 500); } else { @@ -648,6 +650,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener); this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog()); this.binding.port.setText(String.valueOf(Resolver.DEFAULT_PORT_XMPP)); + this.binding.port.setOnFocusChangeListener(mEditTextFocusListener); this.binding.port.addTextChangedListener(mTextWatcher); this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener); this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener); @@ -1142,7 +1145,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat binding.accountPassword.getEditableText().append(this.mAccount.getPassword()); binding.accountPassword.setText(this.mAccount.getPassword()); this.binding.hostname.setText(""); - this.binding.hostname.getEditableText().append(this.mAccount.getHostname()); + if (this.mAccount.getHostname().length() > 0) { + this.binding.hostname.getEditableText().append(this.mAccount.getHostname()); + } else { + this.binding.hostname.getEditableText().append(this.mAccount.getDomain()); + } this.binding.port.setText(""); this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort())); this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); @@ -1298,25 +1305,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { this.binding.pgpFingerprintBox.setVisibility(View.GONE); } - final String otrFingerprint = this.mAccount.getOtrFingerprint(); - if (otrFingerprint != null && Config.supportOtr()) { - if ("otr".equals(messageFingerprint)) { - this.binding.otrFingerprintDesc.setTextColor(ContextCompat.getColor(this, R.color.accent)); - } - this.binding.otrFingerprintBox.setVisibility(View.VISIBLE); - this.binding.otrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); - this.binding.actionCopyToClipboard.setVisibility(View.VISIBLE); - this.binding.actionCopyToClipboard.setOnClickListener(v -> { - if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(otrFingerprint), R.string.otr_fingerprint)) { - ToastCompat.makeText( - EditAccountActivity.this, - R.string.toast_message_otr_fingerprint, - Toast.LENGTH_SHORT).show(); - } - }); - } else { - this.binding.otrFingerprintBox.setVisibility(View.GONE); - } final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint(); if (ownAxolotlFingerprint != null && Config.supportOmemo()) { this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java index 19e0a0f7f..72bf42e3a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -138,7 +138,9 @@ public class ImportBackupActivity extends XmppActivity implements ServiceConnect try { final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri); showEnterPasswordDialog(backupFile, finishOnCancel); - } catch (final IOException | IllegalArgumentException e) { + } catch (final IOException e) { + Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); + } catch (IllegalArgumentException e) { Log.d(Config.LOGTAG, "unable to open backup file " + uri, e); Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); } diff --git a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java index 8b3dce4fd..b821f877c 100644 --- a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java @@ -1,74 +1,289 @@ package eu.siacs.conversations.ui; import android.Manifest; +import android.annotation.TargetApi; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; +import android.os.Build; import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.util.Log; +import android.view.MenuItem; -import androidx.core.app.ActivityCompat; +import androidx.annotation.BoolRes; +import androidx.annotation.NonNull; + +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.api.IMapController; +import org.osmdroid.config.Configuration; +import org.osmdroid.config.IConfigurationProvider; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.CustomZoomButtonsController; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.CopyrightOverlay; +import org.osmdroid.views.overlay.Overlay; import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.ui.util.LocationHelper; +import eu.siacs.conversations.ui.widget.Marker; +import eu.siacs.conversations.ui.widget.MyLocation; +import eu.siacs.conversations.utils.ThemeHelper; public abstract class LocationActivity extends XmppActivity implements LocationListener { - private LocationManager locationManager; - public static final int REQUEST_LOCATION_PERMISSION = 0x682f41; + protected LocationManager locationManager; + protected boolean hasLocationFeature; + + public static final int REQUEST_CODE_CREATE = 0; + public static final int REQUEST_CODE_FAB_PRESSED = 1; + public static final int REQUEST_CODE_SNACKBAR_PRESSED = 2; + + protected static final String KEY_LOCATION = "loc"; + protected static final String KEY_ZOOM_LEVEL = "zoom"; + + protected Location myLoc = null; + private MapView map = null; + protected IMapController mapController = null; + + protected Bitmap marker_icon; + + protected void clearMarkers() { + synchronized (this.map.getOverlays()) { + for (final Overlay overlay : this.map.getOverlays()) { + if (overlay instanceof Marker || overlay instanceof MyLocation) { + this.map.getOverlays().remove(overlay); + } + } + } + } + + + protected void updateLocationMarkers() { + clearMarkers(); + } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final Context ctx = getApplicationContext(); + setTheme(ThemeHelper.find(this)); + + final PackageManager packageManager = ctx.getPackageManager(); + hasLocationFeature = packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION) || + packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) || + packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_NETWORK); this.locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); + this.marker_icon = BitmapFactory.decodeResource(ctx.getResources(), R.drawable.marker); + + // Ask for location permissions if location services are enabled and we're + // just starting the activity (we don't want to keep pestering them on every + // screen rotation or if there's no point because it's disabled anyways). + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && savedInstanceState == null) { + requestPermissions(REQUEST_CODE_CREATE); + } + + final IConfigurationProvider config = Configuration.getInstance(); + config.load(ctx, getPreferences()); + //config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE); + if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) { + config.setHttpProxy(HttpConnectionManager.getProxy()); + } } - protected abstract void gotoLoc() throws UnsupportedOperationException; + @Override + protected void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); - protected abstract void setmLastLocation(final Location location); + final IGeoPoint center = map.getMapCenter(); + outState.putParcelable(KEY_LOCATION, new GeoPoint( + center.getLatitude(), + center.getLongitude() + )); + outState.putDouble(KEY_ZOOM_LEVEL, map.getZoomLevelDouble()); + } + + @Override + protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + if (savedInstanceState.containsKey(KEY_LOCATION)) { + mapController.setCenter(savedInstanceState.getParcelable(KEY_LOCATION)); + } + if (savedInstanceState.containsKey(KEY_ZOOM_LEVEL)) { + mapController.setZoom(savedInstanceState.getDouble(KEY_ZOOM_LEVEL)); + } + } + + protected void setupMapView(MapView mapView, final GeoPoint pos) { + map = mapView; + map.getOverlays().add(new CopyrightOverlay(this)); + map.setTileSource(TileSourceFactory.MAPNIK); + map.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER); + map.setMultiTouchControls(true); + map.setTilesScaledToDpi(false); + mapController = map.getController(); + mapController.setZoom(Config.Map.INITIAL_ZOOM_LEVEL); + mapController.setCenter(pos); + } + + protected void gotoLoc() { + gotoLoc(map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL); + } + + protected abstract void gotoLoc(final boolean setZoomLevel); + + protected abstract void setMyLoc(final Location location); protected void requestLocationUpdates() { - if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + if (!hasLocationFeature || locationManager == null) { return; } - final Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - if (lastKnownLocation != null) { - setmLastLocation(lastKnownLocation); - try { - gotoLoc(); - } catch (final UnsupportedOperationException ignored) { + + Log.d(Config.LOGTAG, "Requesting location updates..."); + final Location lastKnownLocationGps; + final Location lastKnownLocationNetwork; + + try { + if (locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER)) { + lastKnownLocationGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + + if (lastKnownLocationGps != null) { + setMyLoc(lastKnownLocationGps); + } + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA, + Config.Map.LOCATION_FIX_SPACE_DELTA, this); + } else { + lastKnownLocationGps = null; } - } - if (locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER) - && locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, Config.LOCATION_FIX_TIME_DELTA, Config.LOCATION_FIX_SPACE_DELTA, this); - } - if (locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER) - && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, Config.LOCATION_FIX_TIME_DELTA, Config.LOCATION_FIX_SPACE_DELTA, this); - } - // If something else is also querying for location more frequently than we are, the battery is already being - // drained. Go ahead and use the existing locations as often as we can get them. - if (locationManager.getAllProviders().contains(LocationManager.PASSIVE_PROVIDER)) { - locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this); + + if (locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER)) { + lastKnownLocationNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + if (lastKnownLocationNetwork != null && LocationHelper.isBetterLocation(lastKnownLocationNetwork, + lastKnownLocationGps)) { + setMyLoc(lastKnownLocationNetwork); + } + locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA, + Config.Map.LOCATION_FIX_SPACE_DELTA, this); + } + + // If something else is also querying for location more frequently than we are, the battery is already being + // drained. Go ahead and use the existing locations as often as we can get them. + if (locationManager.getAllProviders().contains(LocationManager.PASSIVE_PROVIDER)) { + locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this); + } + } catch (final SecurityException ignored) { + // Do nothing if the users device has no location providers. } } - protected void pauseLocationUpdates() { - locationManager.removeUpdates(this); + protected void pauseLocationUpdates() throws SecurityException { + if (locationManager != null) { + locationManager.removeUpdates(this); + } + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + return super.onOptionsItemSelected(item); } @Override public void onPause() { super.onPause(); - pauseLocationUpdates(); + Configuration.getInstance().save(this, getPreferences()); + map.onPause(); + try { + pauseLocationUpdates(); + } catch (final SecurityException ignored) { + } + } + + protected abstract void updateUi(); + + protected boolean mapAtInitialLoc() { + return map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL; } @Override public void onResume() { super.onResume(); - this.setmLastLocation(null); - if (hasLocationPermission(REQUEST_LOCATION_PERMISSION)) { - requestLocationUpdates(); + Configuration.getInstance().load(this, getPreferences()); + map.onResume(); + this.setMyLoc(null); + requestLocationUpdates(); + updateLocationMarkers(); + updateUi(); + map.setTileSource(TileSourceFactory.MAPNIK); + map.setTilesScaledToDpi(true); + + if (mapAtInitialLoc()) { + gotoLoc(); } } -} \ No newline at end of file + + @TargetApi(Build.VERSION_CODES.M) + protected boolean hasLocationPermissions() { + return (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || + checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED); + } + + @TargetApi(Build.VERSION_CODES.M) + protected void requestPermissions(final int request_code) { + if (!hasLocationPermissions()) { + requestPermissions( + new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + }, + request_code + ); + } + } + + @Override + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + for (int i = 0; i < grantResults.length; i++) { + if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[i]) || + Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[i])) { + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + requestLocationUpdates(); + } + } + } + } + + public SharedPreferences getPreferences() { + return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + } + + protected boolean getBooleanPreference(String name, @BoolRes int res) { + return getPreferences().getBoolean(name, getResources().getBoolean(res)); + } + + protected boolean isLocationEnabled() { + try { + final int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE); + return locationMode != Settings.Secure.LOCATION_MODE_OFF; + } catch (final Settings.SettingNotFoundException e) { + return false; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java index c9c76fb95..f79d3708e 100644 --- a/src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java @@ -5,37 +5,31 @@ import android.content.pm.ActivityInfo; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; -import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CompoundButton; -import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.databinding.DataBindingUtil; import java.security.SecureRandom; -import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.ExecutionException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityMagicCreateBinding; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.ProviderService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.InstallReferrerUtils; import eu.siacs.conversations.xmpp.Jid; public class MagicCreateActivity extends XmppActivity implements TextWatcher, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener { - private void setupHyperlink() { - TextView linkTextView = findViewById(R.id.activity_main_link); - linkTextView.setMovementMethod(LinkMovementMethod.getInstance()); - } - private boolean useOwnProvider = false; private boolean registerFromUri = false; public static final String EXTRA_DOMAIN = "domain"; @@ -86,10 +80,17 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher, Ad } super.onCreate(savedInstanceState); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_magic_create); - final List domains = Arrays.asList(getResources().getStringArray(R.array.domains)); + final List domains = ProviderService.getProviders(); Collections.sort(domains, String::compareToIgnoreCase); final ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_selectable_list_item, domains); - int defaultServer = adapter.getPosition("monocles.de"); + try { + if (new ProviderService().execute().get()) { + adapter.notifyDataSetChanged(); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + int defaultServer = adapter.getPosition(Config.DOMAIN.getRandomServer()); if (registerFromUri && !useOwnProvider && (this.preAuth != null || domain != null)) { binding.server.setEnabled(false); binding.server.setVisibility(View.GONE); @@ -188,7 +189,6 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher, Ad } }); binding.username.addTextChangedListener(this); - setupHyperlink(); } private String updateDomain() { @@ -230,7 +230,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher, Ad private void updateFullJidInformation(String username) { if (useOwnProvider && !registerFromUri) { this.domain = updateDomain(); - } else if (!registerFromUri){ + } else if (!registerFromUri) { this.domain = binding.server.getSelectedItem().toString(); } if (username.trim().isEmpty()) { diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index cdb28954b..5e41b10cd 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.readGranted; + import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.Bundle; @@ -35,9 +38,6 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import me.drakeet.support.toast.ToastCompat; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; - public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState { private final String STATE_SELECTED_ACCOUNT = "selected_account"; @@ -212,6 +212,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) { if (allGranted(grantResults)) { switch (requestCode) { diff --git a/src/main/java/eu/siacs/conversations/ui/MemoryManagementActivity.java b/src/main/java/eu/siacs/conversations/ui/MemoryManagementActivity.java index 988c9dc8c..5b32074be 100644 --- a/src/main/java/eu/siacs/conversations/ui/MemoryManagementActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/MemoryManagementActivity.java @@ -1,5 +1,10 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.persistance.FileBackend.AUDIOS; +import static eu.siacs.conversations.persistance.FileBackend.FILES; +import static eu.siacs.conversations.persistance.FileBackend.IMAGES; +import static eu.siacs.conversations.persistance.FileBackend.VIDEOS; + import android.os.AsyncTask; import android.os.Bundle; import android.widget.ImageButton; @@ -14,31 +19,26 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.utils.UIHelper; -import static eu.siacs.conversations.persistance.FileBackend.AUDIOS; -import static eu.siacs.conversations.persistance.FileBackend.FILES; -import static eu.siacs.conversations.persistance.FileBackend.IMAGES; -import static eu.siacs.conversations.persistance.FileBackend.VIDEOS; - public class MemoryManagementActivity extends XmppActivity { - private TextView disk_storage; - private TextView media_usage; + private static TextView disk_storage; + private static TextView media_usage; private ImageButton delete_media; - private TextView pictures_usage; + private static TextView pictures_usage; private ImageButton delete_pictures; - private TextView videos_usage; + private static TextView videos_usage; private ImageButton delete_videos; - private TextView files_usage; + private static TextView files_usage; private ImageButton delete_files; - private TextView audios_usage; + private static TextView audios_usage; private ImageButton delete_audios; - String totalMemory = "..."; - String mediaUsage = "..."; - String picturesUsage = "..."; - String videosUsage = "..."; - String filesUsage = "..."; - String audiosUsage = "..."; + static String totalMemory = "..."; + static String mediaUsage = "..."; + static String picturesUsage = "..."; + static String videosUsage = "..."; + static String filesUsage = "..."; + static String audiosUsage = "..."; @Override protected void refreshUiReal() { @@ -117,7 +117,7 @@ public class MemoryManagementActivity extends XmppActivity { builder.create().show(); } - class getMemoryUsages extends AsyncTask { + static class getMemoryUsages extends AsyncTask { @Override protected void onPreExecute() { diff --git a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java index 8c32ba4d9..0862196e7 100644 --- a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java @@ -114,7 +114,7 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ @Override public void onAffiliationChangeFailed(Jid jid, int resId) { - displayToast(getString(resId, jid.asBareJid().toEscapedString())); + displayToast(getString(resId, jid.asBareJid().toString())); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index f83bd2219..63c72ab05 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -252,4 +252,4 @@ public class RecordingActivity extends AppCompatActivity implements View.OnClick }); builder.create().show(); } - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 892069d53..87223c014 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.ui; import static java.util.Arrays.asList; import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; -import eu.siacs.conversations.ui.util.Rationals; import android.Manifest; import android.annotation.SuppressLint; @@ -37,6 +36,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import org.checkerframework.checker.nullness.compatqual.NullableDecl; +import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoTrack; @@ -56,6 +56,7 @@ import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.MainThreadExecutor; +import eu.siacs.conversations.ui.util.Rationals; import eu.siacs.conversations.utils.Namespace; import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.TimeFrameUtils; @@ -67,7 +68,6 @@ import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import me.drakeet.support.toast.ToastCompat; - public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { public static final String EXTRA_WITH = "with"; @@ -80,8 +80,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private static final int CALL_DURATION_UPDATE_INTERVAL = 333; - private boolean shouldAllowBack = false; - private static final List END_CARD = Arrays.asList( RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.SECURITY_ERROR, @@ -97,7 +95,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe ); private static final List STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList( RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING + ); + private static final List STATES_CONSIDERED_CONNECTED = Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING + ); + private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.RECONNECTING ); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; @@ -484,10 +492,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return; } } else { - if (shouldAllowBack) { - super.onBackPressed(); - } + endCall(); } + super.onBackPressed(); } @Override @@ -504,7 +511,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private boolean isConnected() { final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED; + return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); } private boolean switchToPictureInPicture() { @@ -533,6 +540,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e); } } + @Override public void onAspectRatioChanged(final Rational rational) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) { @@ -636,8 +644,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe surfaceViewRenderer.setVisibility(View.VISIBLE); try { surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); + } catch (final IllegalStateException e) { + //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); } surfaceViewRenderer.setEnableHardwareScaler(true); } @@ -649,7 +657,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void updateStateDisplay(final RtpEndUserState state, final Set media) { switch (state) { case INCOMING_CALL: - shouldAllowBack = false; Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); if (media.contains(Media.VIDEO)) { setTitle(R.string.rtp_state_incoming_video_call); @@ -658,53 +665,45 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } break; case CONNECTING: - shouldAllowBack = false; setTitle(R.string.rtp_state_connecting); break; case CONNECTED: - shouldAllowBack = false; setTitle(R.string.rtp_state_connected); break; + case RECONNECTING: + setTitle(R.string.rtp_state_reconnecting); + break; case ACCEPTING_CALL: - shouldAllowBack = false; setTitle(R.string.rtp_state_accepting_call); break; case ENDING_CALL: - shouldAllowBack = false; setTitle(R.string.rtp_state_ending_call); break; case FINDING_DEVICE: - shouldAllowBack = false; setTitle(R.string.rtp_state_finding_device); break; case RINGING: - shouldAllowBack = false; setTitle(R.string.rtp_state_ringing); break; case DECLINED_OR_BUSY: - shouldAllowBack = true; setTitle(R.string.rtp_state_declined_or_busy); break; case CONNECTIVITY_ERROR: - shouldAllowBack = true; setTitle(R.string.rtp_state_connectivity_error); break; case CONNECTIVITY_LOST_ERROR: setTitle(R.string.rtp_state_connectivity_lost_error); break; case RETRACTED: - shouldAllowBack = false; setTitle(R.string.rtp_state_retracted); break; case APPLICATION_ERROR: - shouldAllowBack = true; setTitle(R.string.rtp_state_application_failure); break; case SECURITY_ERROR: setTitle(R.string.rtp_state_security_error); break; case ENDED: - shouldAllowBack = true; throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();"); default: throw new IllegalStateException(String.format("State %s has not been handled in UI", state)); @@ -816,7 +815,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @SuppressLint("RestrictedApi") private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set media) { - if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { + if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); if (media.contains(Media.VIDEO)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); @@ -944,14 +943,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.duration.setVisibility(View.GONE); return; } - final long rtpConnectionStarted = connection.getRtpConnectionStarted(); - final long rtpConnectionEnded = connection.getRtpConnectionEnded(); - if (rtpConnectionStarted != 0) { - final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded; - this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false)); - this.binding.duration.setVisibility(View.VISIBLE); - } else { + if (connection.zeroDuration()) { this.binding.duration.setVisibility(View.GONE); + } else { + this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false)); + this.binding.duration.setVisibility(View.VISIBLE); } } @@ -959,17 +955,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { binding.localVideo.setVisibility(View.GONE); binding.localVideo.release(); - binding.remoteVideo.setVisibility(View.GONE); + binding.remoteVideoWrapper.setVisibility(View.GONE); binding.remoteVideo.release(); binding.pipLocalMicOffIndicator.setVisibility(View.GONE); if (isPictureInPicture()) { + binding.appBarLayout.setVisibility(View.GONE); + binding.pipPlaceholder.setVisibility(View.VISIBLE); if (Arrays.asList( RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.CONNECTIVITY_ERROR, RtpEndUserState.SECURITY_ERROR) .contains(state)) { - binding.appBarLayout.setVisibility(View.GONE); - binding.pipPlaceholder.setVisibility(View.VISIBLE); binding.pipWarning.setVisibility(View.VISIBLE); binding.pipWaiting.setVisibility(View.GONE); } else { @@ -983,9 +979,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); return; } - if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) { + if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) { binding.localVideo.setVisibility(View.GONE); - binding.remoteVideo.setVisibility(View.GONE); + binding.remoteVideoWrapper.setVisibility(View.GONE); binding.appBarLayout.setVisibility(View.GONE); binding.pipPlaceholder.setVisibility(View.VISIBLE); binding.pipWarning.setVisibility(View.GONE); @@ -1007,12 +1003,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (remoteVideoTrack.isPresent()) { ensureSurfaceViewRendererIsSetup(binding.remoteVideo); addSink(remoteVideoTrack.get(), binding.remoteVideo); + binding.remoteVideo.setScalingType( + RendererCommon.ScalingType.SCALE_ASPECT_FILL, + RendererCommon.ScalingType.SCALE_ASPECT_FIT + ); if (state == RtpEndUserState.CONNECTED) { binding.appBarLayout.setVisibility(View.GONE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + binding.remoteVideoWrapper.setVisibility(View.VISIBLE); } else { + binding.appBarLayout.setVisibility(View.VISIBLE); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - binding.remoteVideo.setVisibility(View.GONE); + binding.remoteVideoWrapper.setVisibility(View.GONE); } if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) { binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE); @@ -1021,7 +1023,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - binding.remoteVideo.setVisibility(View.GONE); + binding.remoteVideoWrapper.setVisibility(View.GONE); binding.pipLocalMicOffIndicator.setVisibility(View.GONE); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ScanActivity.java b/src/main/java/eu/siacs/conversations/ui/ScanActivity.java index 85f1fabbe..32b374d34 100644 --- a/src/main/java/eu/siacs/conversations/ui/ScanActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ScanActivity.java @@ -76,11 +76,11 @@ public final class ScanActivity extends Activity implements SurfaceTextureListen private static final long AUTO_FOCUS_INTERVAL_MS = 2500L; private static boolean DISABLE_CONTINUOUS_AUTOFOCUS = Build.MODEL.equals("GT-I9100") // Galaxy S2 - || Build.MODEL.equals("SGH-T989") // Galaxy S2 - || Build.MODEL.equals("SGH-T989D") // Galaxy S2 X - || Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket - || Build.MODEL.equals("GT-I9300") // Galaxy S3 - || Build.MODEL.equals("GT-N7000"); // Galaxy Note + || Build.MODEL.equals("SGH-T989") // Galaxy S2 + || Build.MODEL.equals("SGH-T989D") // Galaxy S2 X + || Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket + || Build.MODEL.equals("GT-I9300") // Galaxy S3 + || Build.MODEL.equals("GT-N7000"); // Galaxy Note private final CameraManager cameraManager = new CameraManager(); private ScannerView scannerView; private TextureView previewView; diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java index afa85bd20..5acd1b360 100644 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java @@ -78,20 +78,20 @@ import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard; public class SearchActivity extends XmppActivity implements TextWatcher, OnSearchResultsAvailable, MessageAdapter.OnContactPictureClicked { private static final String EXTRA_SEARCH_TERM = "search-term"; - public static final String EXTRA_CONVERSATION_UUID = "uuid"; + public static final String EXTRA_CONVERSATION_UUID = "uuid"; private ActivitySearchBinding binding; private MessageAdapter messageListAdapter; private final List messages = new ArrayList<>(); private WeakReference selectedMessageReference = new WeakReference<>(null); - private String uuid; + private String uuid; private final ChangeWatcher> currentSearch = new ChangeWatcher<>(); private final PendingItem pendingSearchTerm = new PendingItem<>(); private final PendingItem> pendingSearch = new PendingItem<>(); @Override public void onCreate(final Bundle bundle) { - final Intent intent = getIntent(); - this.uuid = intent == null ? null : Strings.emptyToNull(intent.getStringExtra(EXTRA_CONVERSATION_UUID)); + final Intent intent = getIntent(); + this.uuid = intent == null ? null : Strings.emptyToNull(intent.getStringExtra(EXTRA_CONVERSATION_UUID)); final String searchTerm = bundle == null ? null : bundle.getString(EXTRA_SEARCH_TERM); if (searchTerm != null) { pendingSearchTerm.push(searchTerm); @@ -114,10 +114,10 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc final String term = pendingSearchTerm.pop(); if (term != null) { searchField.append(term); - final List searchTerm = FtsUtils.parse(term); + final List searchTerm = FtsUtils.parse(term); if (xmppConnectionService != null) { if (currentSearch.watch(searchTerm)) { - xmppConnectionService.search(searchTerm, uuid, this); + xmppConnectionService.search(searchTerm, uuid, this); } } else { pendingSearch.push(searchTerm); @@ -125,30 +125,30 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc } searchField.addTextChangedListener(this); searchField.setHint(R.string.search_messages); - searchField.setContentDescription(getString(R.string.search_messages)); + searchField.setContentDescription(getString(R.string.search_messages)); searchField.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); showKeyboard(searchField); return super.onCreateOptionsMenu(menu); } - @Override - public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) { - v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); - AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; - final Message message = this.messages.get(acmi.position); - this.selectedMessageReference = new WeakReference<>(message); - getMenuInflater().inflate(R.menu.search_result_context, menu); - MenuItem copy = menu.findItem(R.id.copy_message); - MenuItem quote = menu.findItem(R.id.quote_message); - MenuItem copyUrl = menu.findItem(R.id.copy_url); - if (message.isGeoUri()) { - copy.setVisible(false); - quote.setVisible(false); - } else { - copyUrl.setVisible(false); - } - super.onCreateContextMenu(menu, v, menuInfo); - } + @Override + public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) { + v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); + AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; + final Message message = this.messages.get(acmi.position); + this.selectedMessageReference = new WeakReference<>(message); + getMenuInflater().inflate(R.menu.search_result_context, menu); + MenuItem copy = menu.findItem(R.id.copy_message); + MenuItem quote = menu.findItem(R.id.quote_message); + MenuItem copyUrl = menu.findItem(R.id.copy_url); + if (message.isGeoUri()) { + copy.setVisible(false); + quote.setVisible(false); + } else { + copyUrl.setVisible(false); + } + super.onCreateContextMenu(menu, v, menuInfo); + } @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -220,7 +220,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc void onBackendConnected() { final List searchTerm = pendingSearch.pop(); if (searchTerm != null && currentSearch.watch(searchTerm)) { - xmppConnectionService.search(searchTerm, uuid,this); + xmppConnectionService.search(searchTerm, uuid,this); } } @@ -253,7 +253,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc return; } if (term.size() > 0) { - xmppConnectionService.search(term, uuid,this); + xmppConnectionService.search(term, uuid,this); } else { MessageSearchTask.cancelRunningTasks(); this.messages.clear(); diff --git a/src/main/java/eu/siacs/conversations/ui/SetSettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SetSettingsActivity.java index 077cc816a..99b2cbc7b 100644 --- a/src/main/java/eu/siacs/conversations/ui/SetSettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SetSettingsActivity.java @@ -1,5 +1,15 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.ui.SettingsActivity.BROADCAST_LAST_ACTIVITY; +import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES; +import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES; +import static eu.siacs.conversations.ui.SettingsActivity.EASY_DOWNLOADER; +import static eu.siacs.conversations.ui.SettingsActivity.FORBID_SCREENSHOTS; +import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE; +import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE; +import static eu.siacs.conversations.ui.SettingsActivity.USE_INNER_STORAGE; +import static eu.siacs.conversations.ui.SettingsActivity.USE_INVIDIOUS; + import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -15,19 +25,12 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivitySetSettingsBinding; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.FirstStartManager; import eu.siacs.conversations.utils.ThemeHelper; -import static eu.siacs.conversations.ui.SettingsActivity.BROADCAST_LAST_ACTIVITY; -import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES; -import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES; -import static eu.siacs.conversations.ui.SettingsActivity.FORBID_SCREENSHOTS; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.USE_INVIDIOUS; - public class SetSettingsActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate { ActivitySetSettingsBinding binding; Account account; @@ -38,6 +41,8 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS static final int CONFIRMMESSAGES = 5; static final int LASTSEEN = 6; static final int INVIDIOUS = 7; + static final int INNER_STORAGE = 8; + static final int EASY_DOWNLOADER_ATTACHMENTS = 9; @Override protected void refreshUiReal() { @@ -69,6 +74,8 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS this.binding.actionInfoConfirmMessages.setOnClickListener(string -> showInfo(CONFIRMMESSAGES)); this.binding.actionInfoLastSeen.setOnClickListener(string -> showInfo(LASTSEEN)); this.binding.actionInfoInvidious.setOnClickListener(string -> showInfo(INVIDIOUS)); + this.binding.actionInfoUsingInnerStorage.setOnClickListener(string -> showInfo(INNER_STORAGE)); + this.binding.actionInfoEasyDownloader.setOnClickListener(string -> showInfo(EASY_DOWNLOADER_ATTACHMENTS)); } private void getDefaults() { @@ -79,6 +86,8 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS this.binding.confirmMessages.setChecked(getResources().getBoolean(R.bool.confirm_messages)); this.binding.lastSeen.setChecked(getResources().getBoolean(R.bool.last_activity)); this.binding.invidious.setChecked(getResources().getBoolean(R.bool.use_invidious)); + this.binding.usingInnerStorage.setChecked(getResources().getBoolean(R.bool.use_inner_storage)); + this.binding.easyDownloader.setChecked(getResources().getBoolean(R.bool.easy_downloader)); } private void next(View view) { @@ -96,7 +105,6 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS } private void showInfo(int setting) { - Log.d(Config.LOGTAG, "STRING " + setting); String title; String message; switch (setting) { @@ -128,6 +136,14 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS title = getString(R.string.pref_use_invidious); message = getString(R.string.pref_use_invidious_summary); break; + case INNER_STORAGE: + title = getString(R.string.pref_use_inner_storage); + message = getString(R.string.pref_use_inner_storage_summary); + break; + case EASY_DOWNLOADER_ATTACHMENTS: + title = getString(R.string.pref_easy_downloader); + message = getString(R.string.pref_easy_downloader_summary); + break; default: title = getString(R.string.error); message = getString(R.string.error); @@ -143,41 +159,16 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS private void setSettings() { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - if (this.binding.forbidScreenshots.isChecked()) { - preferences.edit().putBoolean(FORBID_SCREENSHOTS, true).apply(); - } else { - preferences.edit().putBoolean(FORBID_SCREENSHOTS, false).apply(); - } - if (this.binding.showLinks.isChecked()) { - preferences.edit().putBoolean(SHOW_LINKS_INSIDE, true).apply(); - } else { - preferences.edit().putBoolean(SHOW_LINKS_INSIDE, false).apply(); - } - if (this.binding.showMappreview.isChecked()) { - preferences.edit().putBoolean(SHOW_MAPS_INSIDE, true).apply(); - } else { - preferences.edit().putBoolean(SHOW_MAPS_INSIDE, false).apply(); - } - if (this.binding.chatStates.isChecked()) { - preferences.edit().putBoolean(CHAT_STATES, true).apply(); - } else { - preferences.edit().putBoolean(CHAT_STATES, false).apply(); - } - if (this.binding.confirmMessages.isChecked()) { - preferences.edit().putBoolean(CONFIRM_MESSAGES, true).apply(); - } else { - preferences.edit().putBoolean(CONFIRM_MESSAGES, false).apply(); - } - if (this.binding.lastSeen.isChecked()) { - preferences.edit().putBoolean(BROADCAST_LAST_ACTIVITY, true).apply(); - } else { - preferences.edit().putBoolean(BROADCAST_LAST_ACTIVITY, false).apply(); - } - if (this.binding.invidious.isChecked()) { - preferences.edit().putBoolean(USE_INVIDIOUS, true).apply(); - } else { - preferences.edit().putBoolean(USE_INVIDIOUS, false).apply(); - } + preferences.edit().putBoolean(FORBID_SCREENSHOTS, this.binding.forbidScreenshots.isChecked()).apply(); + preferences.edit().putBoolean(SHOW_LINKS_INSIDE, this.binding.showLinks.isChecked()).apply(); + preferences.edit().putBoolean(SHOW_MAPS_INSIDE, this.binding.showMappreview.isChecked()).apply(); + preferences.edit().putBoolean(CHAT_STATES, this.binding.chatStates.isChecked()).apply(); + preferences.edit().putBoolean(CONFIRM_MESSAGES, this.binding.confirmMessages.isChecked()).apply(); + preferences.edit().putBoolean(BROADCAST_LAST_ACTIVITY, this.binding.lastSeen.isChecked()).apply(); + preferences.edit().putBoolean(USE_INVIDIOUS, this.binding.invidious.isChecked()).apply(); + preferences.edit().putBoolean(USE_INNER_STORAGE, this.binding.usingInnerStorage.isChecked()).apply(); + preferences.edit().putBoolean(EASY_DOWNLOADER, this.binding.easyDownloader.isChecked()).apply(); + FileBackend.switchStorage(xmppConnectionService.usingInnerStorage()); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index bbc61c5cc..fd16fe51f 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -16,11 +16,6 @@ import android.preference.PreferenceCategory; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.util.Log; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.ObjectInputStream; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; @@ -86,16 +81,16 @@ public class SettingsActivity extends XmppActivity implements public static final String CONFIRM_MESSAGES = "confirm_messages"; public static final String INDICATE_RECEIVED = "indicate_received"; public static final String USE_INVIDIOUS = "use_invidious"; + public static final String USE_INNER_STORAGE = "use_inner_storage"; public static final String INVIDIOUS_HOST = "invidious_host"; public static final String MAPPREVIEW_HOST = "mappreview_host"; public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction"; + public static final String ALLOW_MESSAGE_RETRACTION = "allow_message_retraction"; public static final String USE_UNICOLORED_CHATBG = "unicolored_chatbg"; public static final String EASY_DOWNLOADER = "easy_downloader"; public static final String MIN_ANDROID_SDK21_SHOWN = "min_android_sdk21_shown"; public static final String INDIVIDUAL_NOTIFICATION_PREFIX = "individual_notification_set_"; public static final String PAUSE_VOICE = "pause_voice_on_move_from_ear"; - public static final String ENABLE_OTR_ENCRYPTION = "enable_otr_encryption"; - public static final int REQUEST_CREATE_BACKUP = 0xbf8701; public static final int REQUEST_IMPORT_SETTINGS = 0xbf8702; @@ -194,7 +189,7 @@ public class SettingsActivity extends XmppActivity implements if (choices[i] == 0) { entries[i] = getString(R.string.never); } else { - entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); + entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); } } automaticMessageDeletionList.setEntries(entries); @@ -564,6 +559,7 @@ public class SettingsActivity extends XmppActivity implements TREAT_VIBRATE_AS_SILENT, MANUALLY_CHANGE_PRESENCE, BROADCAST_LAST_ACTIVITY); + FileBackend.switchStorage(preferences.getBoolean(USE_INNER_STORAGE, true)); if (name.equals(OMEMO_SETTING)) { OmemoSetting.load(this, preferences); changeOmemoSettingSummary(); @@ -594,7 +590,6 @@ public class SettingsActivity extends XmppActivity implements @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) { @@ -613,10 +608,10 @@ public class SettingsActivity extends XmppActivity implements final Intent intent = new Intent(this, ExportBackupService.class); intent.putExtra("NOTIFY_ON_BACKUP_COMPLETE", notify); ContextCompat.startForegroundService(this, intent); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(R.string.backup_started_message); - builder.setPositiveButton(R.string.ok, null); - builder.create().show(); + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.backup_started_message); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); } @SuppressWarnings({ "unchecked" }) @@ -661,6 +656,11 @@ public class SettingsActivity extends XmppActivity implements ex.printStackTrace(); } } + if (success) { + ToastCompat.makeText(this, R.string.success_import_settings, ToastCompat.LENGTH_SHORT).show(); + } else { + ToastCompat.makeText(this, R.string.error_import_settings, ToastCompat.LENGTH_SHORT).show(); + } return success; } diff --git a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java index aa02bf1c5..7e9b73c00 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java @@ -1,67 +1,63 @@ package eu.siacs.conversations.ui; -import android.annotation.TargetApi; +import android.Manifest; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.location.Address; import android.location.Geocoder; import android.location.Location; import android.location.LocationListener; -import android.location.LocationManager; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.provider.Settings; -import android.text.TextUtils; -import android.webkit.WebView; -import android.widget.Button; +import android.text.Html; +import android.view.View; -import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.databinding.DataBindingUtil; import com.google.android.material.snackbar.Snackbar; +import com.google.common.math.DoubleMath; -import org.jetbrains.annotations.Nullable; +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.util.GeoPoint; import java.lang.ref.WeakReference; +import java.math.RoundingMode; import java.util.List; import java.util.Locale; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.utils.LocationHelper; +import eu.siacs.conversations.databinding.ActivityShareLocactionBinding; +import eu.siacs.conversations.ui.util.LocationHelper; +import eu.siacs.conversations.ui.widget.Marker; +import eu.siacs.conversations.ui.widget.MyLocation; +import eu.siacs.conversations.utils.LocationProvider; import eu.siacs.conversations.utils.ThemeHelper; -import me.drakeet.support.toast.ToastCompat; public class ShareLocationActivity extends LocationActivity implements LocationListener { - LocationManager locationManager; - private Location mLastLocation; - private Button mCancelButton; - private Button mShareButton; - private String mLocationName; private Snackbar snackBar; + private ActivityShareLocactionBinding binding; + private boolean marker_fixed_to_loc = false; + private static final String KEY_FIXED_TO_LOC = "fixed_to_loc"; + private Boolean noAskAgain = false; - private static String getAddress(Context context, Location location) { - double longitude = location.getLongitude(); - double latitude = location.getLatitude(); - String address = ""; - if (latitude != 0 && longitude != 0) { - try { - Geocoder geoCoder = new Geocoder(context, Locale.getDefault()); - List
addresses = geoCoder.getFromLocation(latitude, longitude, 1); - if (addresses != null && addresses.size() > 0) { - Address Address = addresses.get(0); - StringBuilder strAddress = new StringBuilder(""); + @Override + protected void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc); + } - if (Address.getAddressLine(0).length() > 0) { - strAddress.append(Address.getAddressLine(0)); - } - address = strAddress.toString().replace(", ", "
"); - } - } catch (Exception e) { - e.printStackTrace(); - } + @Override + protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) { + this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC); } - return address; } @Override @@ -77,62 +73,99 @@ public class ShareLocationActivity extends LocationActivity implements LocationL @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_share_locaction); - setTitle(getString(R.string.share_location)); - setSupportActionBar(findViewById(R.id.toolbar)); + + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_locaction); + setSupportActionBar((Toolbar) binding.toolbar); configureActionBar(getSupportActionBar()); - locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); + setupMapView(binding.map, LocationProvider.getGeoPoint(this)); - mLocationName = getString(R.string.me); - - mCancelButton = findViewById(R.id.cancel_button); - mCancelButton.setOnClickListener(view -> { + this.binding.cancelButton.setOnClickListener(view -> { setResult(RESULT_CANCELED); finish(); }); - mShareButton = findViewById(R.id.share_button); - mShareButton.setOnClickListener(view -> { - if (mLastLocation != null) { - Intent result = new Intent(); - result.putExtra("latitude", mLastLocation.getLatitude()); - result.putExtra("longitude", mLastLocation.getLongitude()); - result.putExtra("altitude", mLastLocation.getAltitude()); - result.putExtra("accuracy", (int) mLastLocation.getAccuracy()); - setResult(RESULT_OK, result); - finish(); + + this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_sharing_disabled, Snackbar.LENGTH_INDEFINITE); + this.snackBar.setAction(R.string.enable, view -> { + if (isLocationEnabledAndAllowed()) { + updateUi(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) { + requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED); + } else if (!isLocationEnabled()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); } }); + ThemeHelper.fix(this.snackBar); - final CoordinatorLayout snackBarCoordinator = findViewById(R.id.snackbarCoordinator); - if (snackBarCoordinator != null) { - this.snackBar = Snackbar.make(snackBarCoordinator, R.string.location_sharing_disabled, Snackbar.LENGTH_INDEFINITE); - snackBar.setAction(R.string.enable, view -> { - showLocation(null, ""); - if (isLocationEnabled()) { - if (hasLocationPermission(LocationActivity.REQUEST_LOCATION_PERMISSION)) { - requestLocationUpdates(); - } - } else { - setShareButtonEnabled(false); - if (hasLocationPermission(LocationActivity.REQUEST_LOCATION_PERMISSION)) { - requestLocationUpdates(); - } + this.binding.shareButton.setOnClickListener(this::shareLocation); + + this.marker_fixed_to_loc = isLocationEnabledAndAllowed(); + + this.binding.fab.setOnClickListener(view -> { + if (!marker_fixed_to_loc) { + if (!isLocationEnabled()) { startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(REQUEST_CODE_FAB_PRESSED); } - }); - ThemeHelper.fix(this.snackBar); + } + toggleFixedLocation(); + }); + } + + private void shareLocation(final View view) { + final Intent result = new Intent(); + if (marker_fixed_to_loc && myLoc != null) { + result.putExtra("latitude", myLoc.getLatitude()); + result.putExtra("longitude", myLoc.getLongitude()); + result.putExtra("altitude", myLoc.getAltitude()); + result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP)); + } else { + final IGeoPoint markerPoint = this.binding.map.getMapCenter(); + result.putExtra("latitude", markerPoint.getLatitude()); + result.putExtra("longitude", markerPoint.getLongitude()); + } + setResult(RESULT_OK, result); + finish(); + } + + @Override + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (grantResults.length > 0 && + grantResults[0] != PackageManager.PERMISSION_GRANTED && + Build.VERSION.SDK_INT >= 23 && + permissions.length > 0 && + ( + Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) || + Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) || + Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0]) + ) && + !shouldShowRequestPermissionRationale(permissions[0])) { + noAskAgain = true; + } + + if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + updateUi(); + } + + @Override + protected void gotoLoc(final boolean setZoomLevel) { + if (this.myLoc != null && mapController != null) { + if (setZoomLevel) { + mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); + } + mapController.animateTo(new GeoPoint(this.myLoc)); } } @Override - protected void gotoLoc() throws UnsupportedOperationException { - new getAddressAsync(this).execute(); - } - - @Override - protected void setmLastLocation(Location location) { - this.mLastLocation = location; + protected void setMyLoc(final Location location) { + this.myLoc = location; } @Override @@ -141,23 +174,59 @@ public class ShareLocationActivity extends LocationActivity implements LocationL } @Override - public void onResume() { - super.onResume(); - if (isLocationEnabled()) { - this.snackBar.dismiss(); - showLocation(null, ""); + protected void updateLocationMarkers() { + super.updateLocationMarkers(); + if (this.myLoc != null) { + this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); + if (this.marker_fixed_to_loc) { + this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc))); + new getAddressAsync(this, this.myLoc).execute(); + } else { + this.binding.map.getOverlays().add(new Marker(marker_icon)); + new getAddressAsync(this, getMarkerPosition()).execute(); + } } else { - this.snackBar.show(); + this.binding.map.getOverlays().add(new Marker(marker_icon)); + hideAddress(); } - setShareButtonEnabled(false); + } + + private Location getMarkerPosition() { + final IGeoPoint markerPoint = this.binding.map.getMapCenter(); + final Location location = new Location(""); + location.setLatitude(markerPoint.getLatitude()); + location.setLongitude(markerPoint.getLongitude()); + return location; + } + + private void showAddress(final Location myLoc) { + this.binding.address.setText(Html.fromHtml(getAddress(this, myLoc))); + if (Html.fromHtml(getAddress(this, myLoc)).length() > 0) { + this.binding.address.setVisibility(View.VISIBLE); + } else { + hideAddress(); + } + } + + private void hideAddress() { + this.binding.address.setVisibility(View.GONE); } @Override public void onLocationChanged(final Location location) { - if (LocationHelper.isBetterLocation(location, this.mLastLocation)) { - setShareButtonEnabled(true); - this.mLastLocation = location; - gotoLoc(); + if (this.myLoc == null) { + this.marker_fixed_to_loc = true; + } + updateUi(); + if (LocationHelper.isBetterLocation(location, this.myLoc)) { + final Location oldLoc = this.myLoc; + this.myLoc = location; + + // Don't jump back to the users location if they're not moving (more or less). + if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) { + gotoLoc(); + } + updateLocationMarkers(); } } @@ -176,91 +245,92 @@ public class ShareLocationActivity extends LocationActivity implements LocationL } - @TargetApi(Build.VERSION_CODES.KITKAT) - private boolean isLocationEnabledKitkat() { - try { - final int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE); - return locationMode != Settings.Secure.LOCATION_MODE_OFF; - } catch (final Settings.SettingNotFoundException e) { - return false; + private boolean isLocationEnabledAndAllowed() { + return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled(); + } + + private void toggleFixedLocation() { + this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc; + if (this.marker_fixed_to_loc) { + gotoLoc(false); } + updateLocationMarkers(); + updateUi(); } - @SuppressWarnings("deprecation") - private boolean isLocationEnabledLegacy() { - final String locationProviders = Settings.Secure.getString(getContentResolver(), - Settings.Secure.LOCATION_PROVIDERS_ALLOWED); - return !TextUtils.isEmpty(locationProviders); - } - - private boolean isLocationEnabled() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return isLocationEnabledKitkat(); + @Override + protected void updateUi() { + if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) { + this.snackBar.dismiss(); } else { - return isLocationEnabledLegacy(); + this.snackBar.show(); } - } - private void setShareButtonEnabled(final boolean enabled) { - if (enabled) { - this.mShareButton.setEnabled(true); - this.mShareButton.setText(R.string.share); + if (isLocationEnabledAndAllowed()) { + this.binding.fab.setVisibility(View.VISIBLE); + runOnUiThread(() -> { + this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp : + R.drawable.ic_gps_not_fixed_white_24dp); + this.binding.fab.setContentDescription(getResources().getString( + marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location + )); + this.binding.fab.invalidate(); + }); } else { - this.mShareButton.setEnabled(false); - this.mShareButton.setText(R.string.locating); + this.binding.fab.setVisibility(View.GONE); } } - private void showLocation(@Nullable Location location, String address) { - try { - if (location == null && TextUtils.isEmpty(address)) { // no location and no address available - final WebView webView = findViewById(R.id.webView); - webView.getSettings().setJavaScriptEnabled(true); - webView.loadUrl("file:///android_asset/map.html"); - } else if (location != null && TextUtils.isEmpty(address)) { // location but no address available - String LocationName = "" + mLocationName + ""; - final WebView webView = findViewById(R.id.webView); - webView.getSettings().setJavaScriptEnabled(true); - webView.loadUrl("javascript:toCoordinates(" + mLastLocation.getLatitude() + "," + mLastLocation.getLongitude() + "," + "'" + LocationName + "'" + ");"); - } else if (location != null && !TextUtils.isEmpty(address)) { // location and address available - String LocationName = "" + mLocationName + "
" + address; - final WebView webView = findViewById(R.id.webView); - webView.getSettings().setJavaScriptEnabled(true); - webView.loadUrl("javascript:toCoordinates(" + mLastLocation.getLatitude() + "," + mLastLocation.getLongitude() + "," + "'" + LocationName + "'" + ");"); + private static String getAddress(final Context context, final Location location) { + final double longitude = location.getLongitude(); + final double latitude = location.getLatitude(); + String address = ""; + if (latitude != 0 && longitude != 0) { + try { + final Geocoder geoCoder = new Geocoder(context, Locale.getDefault()); + final List
addresses = geoCoder.getFromLocation(latitude, longitude, 1); + if (addresses != null && addresses.size() > 0) { + final Address Address = addresses.get(0); + StringBuilder strAddress = new StringBuilder(""); + + if (Address.getAddressLine(0).length() > 0) { + strAddress.append(Address.getAddressLine(0)); + } + address = strAddress.toString().replace(", ", "
"); + } + } catch (Exception e) { + e.printStackTrace(); } - } catch (Exception e) { - e.printStackTrace(); - ToastCompat.makeText(this, R.string.error, ToastCompat.LENGTH_LONG); } + return address; } private class getAddressAsync extends AsyncTask { String address = null; + Location location; private WeakReference activityReference; - getAddressAsync(ShareLocationActivity context) { + getAddressAsync(final ShareLocationActivity context, final Location location) { activityReference = new WeakReference<>(context); + this.location = location; } @Override protected void onPreExecute() { super.onPreExecute(); - showLocation(mLastLocation, ""); } @Override protected Void doInBackground(Void... params) { - if (mLastLocation != null) { - address = getAddress(ShareLocationActivity.this, mLastLocation); - } + address = getAddress(ShareLocationActivity.this, this.location); return null; } @Override protected void onPostExecute(Void result) { super.onPostExecute(result); - showLocation(mLastLocation, address); + showAddress(this.location); } } -} +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 015712073..d7ed5ee39 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -33,7 +33,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer refreshUi(); } - private class Share { + private static class Share { + public String type; ArrayList uris = new ArrayList<>(); public String account; public String contact; @@ -65,6 +66,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (requestCode == REQUEST_STORAGE_PERMISSION) { @@ -135,6 +137,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer } else if (type != null && uri != null) { this.share.uris.clear(); this.share.uris.add(uri); + this.share.type = type; } else { this.share.text = text; this.share.asQuote = asQuote; @@ -187,6 +190,9 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer intent.setAction(Intent.ACTION_SEND_MULTIPLE); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (share.type != null) { + intent.putExtra(ConversationsActivity.EXTRA_TYPE, share.type); + } } else if (share.text != null) { intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); intent.putExtra(Intent.EXTRA_TEXT, share.text); diff --git a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java index ad0dbef9c..2570fe960 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java @@ -1,24 +1,28 @@ package eu.siacs.conversations.ui; import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.location.Address; import android.location.Geocoder; import android.location.Location; +import android.location.LocationListener; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.TextUtils; -import android.util.Log; +import android.text.Html; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; -import android.webkit.WebView; +import android.view.View; -import com.google.android.material.floatingactionbutton.FloatingActionButton; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.databinding.DataBindingUtil; + +import org.osmdroid.util.GeoPoint; import java.lang.ref.WeakReference; import java.util.List; @@ -26,79 +30,107 @@ import java.util.Locale; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; +import eu.siacs.conversations.databinding.ActivityShowLocationBinding; +import eu.siacs.conversations.ui.util.LocationHelper; +import eu.siacs.conversations.ui.widget.Marker; +import eu.siacs.conversations.ui.widget.MyLocation; +import eu.siacs.conversations.utils.LocationProvider; import me.drakeet.support.toast.ToastCompat; -public class ShowLocationActivity extends XmppActivity { - FloatingActionButton fab; - private Location location; - private String mLocationName; - private static String getAddress(Context context, Location location) { - double longitude = location.getLongitude(); - double latitude = location.getLatitude(); - String address = ""; - if (latitude != 0 && longitude != 0) { - try { - Geocoder geoCoder = new Geocoder(context, Locale.getDefault()); - List
addresses = geoCoder.getFromLocation(latitude, longitude, 1); - if (addresses != null && addresses.size() > 0) { - Address Address = addresses.get(0); - StringBuilder strAddress = new StringBuilder(""); +public class ShowLocationActivity extends LocationActivity implements LocationListener { - if (Address.getAddressLine(0).length() > 0) { - strAddress.append(Address.getAddressLine(0)); - } - address = strAddress.toString().replace(", ", "
"); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - return address; + private GeoPoint loc = LocationProvider.FALLBACK; + private ActivityShowLocationBinding binding; + private String name; + + + private Uri createGeoUri() { + return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude()); } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_show_locaction); - setTitle(getString(R.string.show_location)); - setSupportActionBar(findViewById(R.id.toolbar)); + + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_show_location); + setSupportActionBar((Toolbar) binding.toolbar); + configureActionBar(getSupportActionBar()); - showLocation(null, ""); - Intent intent = getIntent(); + setupMapView(this.binding.map, this.loc); - this.mLocationName = intent != null ? intent.getStringExtra("name") : null; + this.binding.fab.setOnClickListener(view -> startNavigation()); - if (intent != null && intent.hasExtra("longitude") && intent.hasExtra("latitude")) { - double longitude = intent.getDoubleExtra("longitude", 0); - double latitude = intent.getDoubleExtra("latitude", 0); - this.location = new Location(""); - this.location.setLatitude(latitude); - this.location.setLongitude(longitude); - Log.d(Config.LOGTAG, "Location: lat: " + latitude + " long: " + longitude); - markAndCenterOnLocation(this.location); - fab = findViewById(R.id.fab); - fab.setOnClickListener(v -> { - navigate(this.location); - }); + final Intent intent = getIntent(); + if (intent != null) { + this.name = intent.hasExtra("name") ? intent.getStringExtra("name") : null; + if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) { + final double longitude = intent.getDoubleExtra("longitude", 0); + final double latitude = intent.getDoubleExtra("latitude", 0); + this.loc = new GeoPoint(latitude, longitude); + } + + } + updateLocationMarkers(); + } + + @Override + protected void gotoLoc(final boolean setZoomLevel) { + if (this.loc != null && mapController != null) { + if (setZoomLevel) { + mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); + } + mapController.animateTo(new GeoPoint(this.loc)); } } - private void markAndCenterOnLocation(final Location location) { - if (location == null) { - Log.d(Config.LOGTAG, "No location given"); - return; + @Override + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + updateUi(); + } + + @Override + protected void setMyLoc(final Location location) { + this.myLoc = location; + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_show_location, menu); + updateUi(); + return true; + } + + @Override + protected void updateLocationMarkers() { + super.updateLocationMarkers(); + if (this.myLoc != null) { + this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); } - double longitude = location.getLongitude(); - double latitude = location.getLatitude(); - if (latitude != 0 && longitude != 0) { - new getAddressAsync(this).execute(); + this.binding.map.getOverlays().add(new Marker(this.marker_icon, this.loc)); + new getAddressAsync(this, this.loc, this.name).execute(); + } + + private void showAddress(final GeoPoint loc, final String name) { + this.binding.address.setText(Html.fromHtml(getAddress(this, loc, name))); + if (Html.fromHtml(getAddress(this, loc, name)).length() > 0) { + this.binding.address.setVisibility(View.VISIBLE); + } else { + hideAddress(); } } - public SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + private void hideAddress() { + this.binding.address.setVisibility(View.GONE); + } + + @Override + public void onPause() { + super.onPause(); } @Override @@ -112,89 +144,140 @@ public class ShowLocationActivity extends XmppActivity { } @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: - finish(); + case R.id.action_copy_location: + final ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + if (clipboard != null) { + final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString()); + clipboard.setPrimaryClip(clip); + ToastCompat.makeText(this, R.string.url_copied_to_clipboard, ToastCompat.LENGTH_SHORT).show(); + } + return true; + case R.id.action_share_location: + final Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString()); + shareIntent.setType("text/plain"); + try { + startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with))); + } catch (final ActivityNotFoundException e) { + //This should happen only on faulty androids because normally chooser is always available + ToastCompat.makeText(this, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); + } return true; } return super.onOptionsItemSelected(item); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - return true; + private void startNavigation() { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse( + "google.navigation:q=" + + this.loc.getLatitude() + "," + this.loc.getLongitude() + ))); } @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); + protected void updateUi() { + final Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=0,0")); + final ComponentName component = i.resolveActivity(getPackageManager()); + this.binding.fab.setVisibility(component == null ? View.GONE : View.VISIBLE); } - private void showLocation(Location location, String address) { - try { - if (location != null && TextUtils.isEmpty(address)) { // location but no address available - String LocationName = "" + mLocationName + ""; - final WebView webView = findViewById(R.id.webView); - webView.getSettings().setJavaScriptEnabled(true); - webView.loadUrl("file:///android_asset/map.html?lat=" + location.getLatitude() + "&lon=" + location.getLongitude() + "&name=" + LocationName); - } else if (location != null && !TextUtils.isEmpty(address)) { // location and address available - String LocationName = "" + mLocationName + "
" + address; - final WebView webView = findViewById(R.id.webView); - webView.getSettings().setJavaScriptEnabled(true); - webView.loadUrl("javascript:toCoordinates(" + location.getLatitude() + "," + location.getLongitude() + "," + "'" + LocationName + "'" + ");"); + @Override + public void onLocationChanged(final Location location) { + if (LocationHelper.isBetterLocation(location, this.myLoc)) { + this.myLoc = location; + updateLocationMarkers(); + } + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) { + + } + + @Override + public void onProviderEnabled(final String provider) { + + } + + @Override + public void onProviderDisabled(final String provider) { + + } + + private static String getAddress(final Context context, final GeoPoint location, final String name) { + final double longitude = location.getLongitude(); + final double latitude = location.getLatitude(); + String address = ""; + if (latitude != 0 && longitude != 0) { + try { + final Geocoder geoCoder = new Geocoder(context, Locale.getDefault()); + final List
addresses = geoCoder.getFromLocation(latitude, longitude, 1); + if (addresses != null && addresses.size() > 0) { + final Address Address = addresses.get(0); + StringBuilder strAddress = new StringBuilder(""); + if (name != null && name.length() > 0) { + strAddress.append(""); + strAddress.append(name); + strAddress.append(":
"); + } + if (Address.getAddressLine(0).length() > 0) { + strAddress.append(Address.getAddressLine(0)); + } + address = strAddress.toString().replace(", ", "
"); + } else { + StringBuilder strAddress = new StringBuilder(""); + if (name != null && name.length() > 0) { + strAddress.append(""); + strAddress.append(name); + strAddress.append(""); + } + address = strAddress.toString(); + } + } catch (Exception e) { + e.printStackTrace(); + StringBuilder strAddress = new StringBuilder(""); + if (name != null && name.length() > 0) { + strAddress.append(""); + strAddress.append(name); + strAddress.append(""); + } + address = strAddress.toString(); } - } catch (Exception e) { - e.printStackTrace(); - ToastCompat.makeText(this, R.string.error, ToastCompat.LENGTH_LONG); - } - } - - private void navigate(Location location) { - if (location == null) { - Log.d(Config.LOGTAG, "No location given"); - return; - } - double longitude = location.getLongitude(); - double latitude = location.getLatitude(); - try { - Intent intent = new Intent(android.content.Intent.ACTION_VIEW, Uri.parse("google.navigation:q=" + String.valueOf(latitude) + "," + String.valueOf(longitude))); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(this, R.string.no_application_found_to_display_location, ToastCompat.LENGTH_SHORT).show(); } + return address; } private class getAddressAsync extends AsyncTask { String address = null; + String name = null; + GeoPoint location; private WeakReference activityReference; - getAddressAsync(ShowLocationActivity context) { + getAddressAsync(final ShowLocationActivity context, final GeoPoint location, final String name) { activityReference = new WeakReference<>(context); + this.location = location; + this.name = name; } @Override protected void onPreExecute() { super.onPreExecute(); - showLocation(location, ""); } @Override protected Void doInBackground(Void... params) { - address = getAddress(ShowLocationActivity.this, location); + address = getAddress(ShowLocationActivity.this, this.location, this.name); return null; } @Override protected void onPostExecute(Void result) { super.onPostExecute(result); - showLocation(location, address); + showAddress(this.location, this.name); } } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 613659af1..29ec7299e 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -9,6 +9,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.ColorStateList; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -31,15 +32,18 @@ import android.widget.AutoCompleteTextView; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ListView; +import android.widget.PopupMenu; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -49,6 +53,8 @@ import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.google.android.material.textfield.TextInputLayout; +import com.leinardi.android.speeddial.SpeedDialActionItem; +import com.leinardi.android.speeddial.SpeedDialView; import java.util.ArrayList; import java.util.Collections; @@ -94,17 +100,17 @@ public class StartConversationActivity extends XmppActivity implements XmppConne public int conference_context_id; public int contact_context_id; private ListPagerAdapter mListPagerAdapter; - private final List contacts = new ArrayList<>(); + private final List contacts = new ArrayList<>(); private ListItemAdapter mContactsAdapter; - private final List conferences = new ArrayList<>(); + private final List conferences = new ArrayList<>(); private ListItemAdapter mConferenceAdapter; - private final List mActivatedAccounts = new ArrayList<>(); + private final List mActivatedAccounts = new ArrayList<>(); private EditText mSearchEditText; - private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false); - private final AtomicBoolean mOpenedFab = new AtomicBoolean(false); + private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false); + private final AtomicBoolean mOpenedFab = new AtomicBoolean(false); private boolean mHideOfflineContacts = false; private boolean createdByViewIntent = false; - private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { @@ -133,7 +139,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne return true; } }; - private final TextWatcher mSearchTextWatcher = new TextWatcher() { + private final TextWatcher mSearchTextWatcher = new TextWatcher() { @Override public void afterTextChanged(Editable editable) { @@ -149,7 +155,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } }; private MenuItem mMenuSearchView; - private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { + private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { @Override public void onTagClicked(String tag) { if (mMenuSearchView != null) { @@ -162,7 +168,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne }; private Pair mPostponedActivityResult; private Toast mToast; - private final UiCallback mAdhocConferenceCallback = new UiCallback() { + private final UiCallback mAdhocConferenceCallback = new UiCallback() { @Override public void success(final Conversation conversation) { runOnUiThread(() -> { @@ -182,7 +188,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne }; private ActivityStartConversationBinding binding; - private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() { + private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { int pos = binding.startConversationViewPager.getCurrentItem(); @@ -263,7 +269,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne Toolbar toolbar = (Toolbar) binding.toolbar; setSupportActionBar(toolbar); configureActionBar(getSupportActionBar()); - binding.speedDial.inflate(R.menu.start_conversation_fab_submenu); + inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu); binding.tabLayout.setupWithViewPager(binding.startConversationViewPager); binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override @@ -330,6 +336,22 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } return false; }); + binding.speedDial.getMainFab().setSupportImageTintList(ColorStateList.valueOf(getResources().getColor(R.color.realwhite))); + } + + private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) { + speedDialView.clearActionItems(); + final PopupMenu popupMenu = new PopupMenu(this, new View(this)); + popupMenu.inflate(menuRes); + final Menu menu = popupMenu.getMenu(); + for (int i = 0; i < menu.size(); i++) { + final MenuItem menuItem = menu.getItem(i); + final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon()) + .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null) + .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) + .create(); + speedDialView.addActionItem(actionItem); + } } public static boolean isValidJid(String input) { @@ -623,10 +645,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } if (binding.startConversationViewPager.getCurrentItem() == 0) { mSearchEditText.setHint(R.string.search_contacts); - mSearchEditText.setContentDescription(getString(R.string.search_contacts)); + mSearchEditText.setContentDescription(getString(R.string.search_contacts)); } else { mSearchEditText.setHint(R.string.search_bookmarks); - mSearchEditText.setContentDescription(getString(R.string.search_bookmarks)); + mSearchEditText.setContentDescription(getString(R.string.search_bookmarks)); } } diff --git a/src/main/java/eu/siacs/conversations/ui/UpdaterActivity.java b/src/main/java/eu/siacs/conversations/ui/UpdaterActivity.java new file mode 100644 index 000000000..71cea56e2 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/UpdaterActivity.java @@ -0,0 +1,443 @@ +package eu.siacs.conversations.ui; + +import android.Manifest; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.util.Log; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.http.NoSSLv3SocketFactory; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.util.CustomTab; +import eu.siacs.conversations.utils.WakeLockHelper; +import me.drakeet.support.toast.ToastCompat; + +import static eu.siacs.conversations.Config.monocles; +import static eu.siacs.conversations.http.HttpConnectionManager.getProxy; +import static eu.siacs.conversations.services.XmppConnectionService.FDroid; +import static eu.siacs.conversations.services.XmppConnectionService.PlayStore; + +public class UpdaterActivity extends XmppActivity { + static final private String FileName = "update.apk"; + String appURI = ""; + String changelog = ""; + Integer filesize = 0; + String store; + ProgressDialog mProgressDialog; + DownloadTask downloadTask; + TextView textView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + //set activity + setContentView(R.layout.activity_updater); + this.mTheme = findTheme(); + setTheme(this.mTheme); + + textView = findViewById(R.id.updater); + + mProgressDialog = new ProgressDialog(UpdaterActivity.this) { + //show warning on back pressed + @Override + public void onBackPressed() { + showCancelDialog(); + } + }; + mProgressDialog.setMessage(getString(R.string.download_started)); + mProgressDialog.setProgressNumberFormat(null); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + mProgressDialog.setCancelable(false); + mProgressDialog.setCanceledOnTouchOutside(false); + } + + @Override + protected void refreshUiReal() { + //ignored + } + + @Override + protected void onStart() { + super.onStart(); + this.mTheme = findTheme(); + setTheme(this.mTheme); + setTitle(getString(R.string.update_service)); + textView.setText(R.string.update_info); + setSupportActionBar(findViewById(R.id.toolbar)); + configureActionBar(getSupportActionBar()); + if (getIntent() != null && getIntent().getStringExtra("update").equals("PixArtMessenger_UpdateService")) { + try { + appURI = getIntent().getStringExtra("url"); + } catch (Exception e) { + ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); + UpdaterActivity.this.finish(); + } + try { + changelog = getIntent().getStringExtra("changelog"); + } catch (Exception e) { + ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); + UpdaterActivity.this.finish(); + } + try { + store = getIntent().getStringExtra("store"); + } catch (Exception e) { + store = null; + } + //delete old downloaded localVersion files + File dir = new File(FileBackend.getAppUpdateDirectory()); + if (dir.isDirectory()) { + String[] children = dir.list(); + for (String aChildren : children) { + Log.d(Config.LOGTAG, "AppUpdater: delete old update files " + aChildren + " in " + dir); + new File(dir, aChildren).delete(); + } + } + + //oh yeah we do need an upgrade, let the user know send an alert message + final AlertDialog.Builder builder = new AlertDialog.Builder(UpdaterActivity.this); + builder.setCancelable(false); + //open link to changelog + //if the user agrees to upgrade + builder.setMessage(getString(R.string.install_update)) + .setPositiveButton(R.string.update, (dialog, id) -> { + Log.d(Config.LOGTAG, "AppUpdater: downloading " + FileName + " from " + appURI); + //ask for permissions on devices >= SDK 23 + if (isStoragePermissionGranted() && isNetworkAvailable(getApplicationContext())) { + //start downloading the file using the download manager + if (store != null && store.equalsIgnoreCase(PlayStore)) { + Uri uri = Uri.parse("market://details?id=" + getString(R.string.applicationId)); + Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri); + PackageManager manager = getApplicationContext().getPackageManager(); + List infos = manager.queryIntentActivities(marketIntent, 0); + if (infos.size() > 0) { + startActivity(marketIntent); + overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + } else { + try { + uri = Uri.parse("https://" + monocles()); + CustomTab.openTab(this, uri, isDarkTheme()); + } catch (Exception e) { + ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); + } + } + } else if (store != null && store.equalsIgnoreCase(FDroid)) { + Uri uri = Uri.parse("https://f-droid.org/de/packages/" + getString(R.string.applicationId) + "/"); + Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri); + PackageManager manager = getApplicationContext().getPackageManager(); + List infos = manager.queryIntentActivities(marketIntent, 0); + if (infos.size() > 0) { + startActivity(marketIntent); + overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + } else { + uri = Uri.parse("https://" + monocles()); + try { + CustomTab.openTab(this, uri, isDarkTheme()); + } catch (Exception e) { + ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); + } + } + } else { + ToastCompat.makeText(getApplicationContext(), getText(R.string.download_started), ToastCompat.LENGTH_LONG).show(); + downloadTask = new DownloadTask(UpdaterActivity.this); + downloadTask.execute(appURI); + } + } else { + Log.d(Config.LOGTAG, "AppUpdater: failed - has storage permissions " + isStoragePermissionGranted() + " and internet " + isNetworkAvailable(getApplicationContext())); + } + }) + .setNeutralButton(R.string.changelog, (dialog, id) -> { + Uri uri = Uri.parse(Config.CHANGELOG_URL); // missing 'http://' will cause crash + try { + CustomTab.openTab(this, uri, isDarkTheme()); + } catch (Exception e) { + ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); + } finally { + //restart updater to show dialog again after coming back after opening changelog + recreate(); + } + }) + .setNegativeButton(R.string.remind_later, (dialog, id) -> { + // User cancelled the dialog + UpdaterActivity.this.finish(); + }); + //show the alert message + builder.create().show(); + } else { + ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); + UpdaterActivity.this.finish(); + } + } + + @Override + void onBackendConnected() { + //ignored + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + } + + //check for internet connection + private boolean isNetworkAvailable(Context context) { + ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity != null) { + NetworkInfo[] info = connectivity.getAllNetworkInfo(); + if (info != null) { + for (NetworkInfo anInfo : info) { + if (anInfo.getState() == NetworkInfo.State.CONNECTED) { + return true; + } + } + } + } + return false; + } + + public boolean isStoragePermissionGranted() { + if (Build.VERSION.SDK_INT >= 23) { + if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + return true; + } else { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); + return false; + } + } else { //permission is automatically granted on sdk<23 upon installation + return true; + } + } + + //show warning on back pressed + @Override + public void onBackPressed() { + showCancelDialog(); + } + + private void showCancelDialog() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.cancel_update) + .setCancelable(false) + .setPositiveButton(R.string.yes, (dialog, id) -> { + if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) { + downloadTask.cancel(true); + } + if (mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + } + UpdaterActivity.this.finish(); + }) + .setNegativeButton(R.string.no, (dialog, id) -> dialog.cancel()); + final AlertDialog alert = builder.create(); + alert.show(); + } + + @Override + public void onPause() { + super.onPause(); + if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) { + downloadTask.cancel(true); + } + UpdaterActivity.this.finish(); + } + + @Override + protected void onStop() { + super.onStop(); + if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) { + downloadTask.cancel(true); + } + UpdaterActivity.this.finish(); + } + + private class DownloadTask extends AsyncTask { + XmppActivity activity; + File dir = new File(FileBackend.getAppUpdateDirectory()); + File file = new File(dir, FileName); + XmppConnectionService xmppConnectionService; + private Context context; + private PowerManager.WakeLock mWakeLock; + private long startTime = 0; + private boolean mUseTor; + + DownloadTask(Context context) { + this.context = context; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + startTime = System.currentTimeMillis(); + // take CPU lock to prevent CPU from going off if the user + // presses the power button during download + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (pm != null) { + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getName()); + mWakeLock.acquire(); + mUseTor = xmppConnectionService != null && xmppConnectionService.useTorToConnect(); + } + mProgressDialog.show(); + } + + @Override + protected void onProgressUpdate(Integer... progress) { + super.onProgressUpdate(progress); + // if we get here, length is known, now set indeterminate to false + mProgressDialog.setIndeterminate(false); + mProgressDialog.setMax(100); + mProgressDialog.setProgress(progress[0]); + } + + @Override + protected String doInBackground(String... sUrl) { + InputStream is = null; + OutputStream os = null; + SSLContext sslcontext = null; + SSLSocketFactory NoSSLv3Factory = null; + try { + sslcontext = SSLContext.getInstance("TLSv1"); + if (sslcontext != null) { + sslcontext.init(null, null, null); + NoSSLv3Factory = new NoSSLv3SocketFactory(sslcontext.getSocketFactory()); + } + } catch (NoSuchAlgorithmException | KeyManagementException e) { + e.printStackTrace(); + } + HttpsURLConnection.setDefaultSSLSocketFactory(NoSSLv3Factory); + HttpsURLConnection connection = null; + try { + Log.d(Config.LOGTAG, "AppUpdater: save file to " + file.toString()); + Log.d(Config.LOGTAG, "AppUpdater: download update from url: " + sUrl[0] + " to file name: " + file.toString()); + + URL url = new URL(sUrl[0]); + + if (mUseTor) { + connection = (HttpsURLConnection) url.openConnection(getProxy()); + } else { + connection = (HttpsURLConnection) url.openConnection(); + } + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setRequestProperty("User-agent", System.getProperty("http.agent")); + connection.connect(); + + // expect HTTP 200 OK, so we don't mistakenly save error report + // instead of the file + if (connection.getResponseCode() != HttpsURLConnection.HTTP_OK) { + ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); + return connection.getResponseCode() + ": " + connection.getResponseMessage(); + } + + // this will be useful to display download percentage + // might be -1: server did not report the length + int fileLength = connection.getContentLength(); + + // create folders + File parentDirectory = file.getParentFile(); + if (parentDirectory.mkdirs()) { + Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath()); + } + + // download the file + is = connection.getInputStream(); + os = new FileOutputStream(file); + + byte[] data = new byte[4096]; + long total = 0; + int count; + while ((count = is.read(data)) != -1) { + // allow canceling with back button + if (isCancelled()) { + is.close(); + return "canceled"; + } + total += count; + // publishing the progress.... + if (fileLength > 0) // only if total length is known + publishProgress((int) (total * 100 / fileLength)); + os.write(data, 0, count); + } + } catch (Exception e) { + e.printStackTrace(); + return e.toString(); + } finally { + try { + if (os != null) + os.close(); + if (is != null) + is.close(); + } catch (IOException ignored) { + } + + if (connection != null) + connection.disconnect(); + } + return null; + } + + @Override + protected void onPostExecute(String result) { + WakeLockHelper.release(mWakeLock); + mProgressDialog.dismiss(); + if (result != null) { + ToastCompat.makeText(getApplicationContext(), getString(R.string.failed), ToastCompat.LENGTH_LONG).show(); + Log.d(Config.LOGTAG, "AppUpdater: failed with " + result); + UpdaterActivity.this.finish(); + } else { + Log.d(Config.LOGTAG, "AppUpdater: download ready in " + ((System.currentTimeMillis() - startTime) / 1000) + " sec"); + + //start the installation of the latest localVersion + Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + installIntent.setDataAndType(FileBackend.getUriForFile(UpdaterActivity.this, file), "application/vnd.android.package-archive"); + installIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + installIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); + installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + installIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(installIntent); + overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + UpdaterActivity.this.finish(); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index f9b7c38c3..c61f6fad7 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -7,17 +7,27 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.util.Log; +import android.view.View; +import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import androidx.databinding.DataBindingUtil; import com.google.common.base.Strings; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityUriHandlerBinding; +import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.ProvisioningUtils; @@ -25,6 +35,11 @@ import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import me.drakeet.support.toast.ToastCompat; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; public class UriHandlerActivity extends AppCompatActivity { @@ -34,7 +49,9 @@ public class UriHandlerActivity extends AppCompatActivity { private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789; private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790; private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n"); - private boolean handled = false; + private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>"); + private ActivityUriHandlerBinding binding; + private Call call; public static void scan(final Activity activity) { scan(activity, false); @@ -77,9 +94,7 @@ public class UriHandlerActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - this.handled = savedInstanceState != null && savedInstanceState.getBoolean("handled", false); - getLayoutInflater().inflate(R.layout.toolbar, findViewById(android.R.id.content)); - setSupportActionBar(findViewById(R.id.toolbar)); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler); } @Override @@ -88,23 +103,17 @@ public class UriHandlerActivity extends AppCompatActivity { handleIntent(getIntent()); } - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - savedInstanceState.putBoolean("handled", this.handled); - super.onSaveInstanceState(savedInstanceState); - } - @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); handleIntent(intent); } - private void handleUri(Uri uri) { - handleUri(uri, false); + private boolean handleUri(final Uri uri) { + return handleUri(uri, false); } - private void handleUri(Uri uri, final boolean scanned) { + private boolean handleUri(final Uri uri, final boolean scanned) { final Intent intent; final XmppUri xmppUri = new XmppUri(uri); final List accounts = DatabaseBackend.getInstance(this).getAccountJids(true); @@ -113,40 +122,39 @@ public class UriHandlerActivity extends AppCompatActivity { final Jid jid = xmppUri.getJid(); if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) { - ToastCompat.makeText(this, R.string.account_already_exists, ToastCompat.LENGTH_LONG).show(); - return; + showError(R.string.account_already_exists); + return false; } intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth, true); startActivity(intent); - return; + return true; } if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); startActivity(intent); - return; + return true; } + } else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { + showError(R.string.account_registrations_are_not_supported); + return false; } if (accounts.size() == 0) { if (xmppUri.isValidJid()) { intent = SignupUtils.getSignUpIntent(this); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); startActivity(intent); + return true; } else { - ToastCompat.makeText(this, R.string.invalid_jid, ToastCompat.LENGTH_SHORT).show(); + showError(R.string.invalid_jid); + return false; } - return; } if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) { final Jid jid = xmppUri.getJid(); final String body = xmppUri.getBody(); if (jid != null) { - Class clazz; - try { - clazz = Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity"); - } catch (ClassNotFoundException e) { - clazz = null; - } + final Class clazz = findShareViaAccountClass(); if (clazz != null) { intent = new Intent(this, clazz); intent.putExtra("contact", jid.toEscapedString()); @@ -176,32 +184,93 @@ public class UriHandlerActivity extends AppCompatActivity { intent.putExtra("scanned", scanned); intent.setData(uri); } else { - ToastCompat.makeText(this, R.string.invalid_jid, ToastCompat.LENGTH_SHORT).show(); - return; + showError(R.string.invalid_jid); + return false; } startActivity(intent); + return true; } - private void handleIntent(Intent data) { - if (handled) { + private void checkForLinkHeader(final HttpUrl url) { + Log.d(Config.LOGTAG, "checking for link header on " + url); + this.call = HttpConnectionManager.OK_HTTP_CLIENT.newCall(new Request.Builder() + .url(url) + .head() + .build()); + this.call.enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + Log.d(Config.LOGTAG, "unable to check HTTP url", e); + showError(R.string.no_xmpp_adddress_found); + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + if (response.isSuccessful()) { + final String linkHeader = response.header("Link"); + if (linkHeader != null && processLinkHeader(linkHeader)) { + return; + } + } + showError(R.string.no_xmpp_adddress_found); + } + }); + + } + + private boolean processLinkHeader(final String header) { + final Matcher matcher = LINK_HEADER_PATTERN.matcher(header); + if (matcher.find()) { + final String group = matcher.group(); + final String link = group.substring(1, group.length() - 1); + if (handleUri(Uri.parse(link))) { + finish(); + return true; + } + } + return false; + } + + private void showError(@StringRes int error) { + this.binding.progress.setVisibility(View.INVISIBLE); + this.binding.error.setText(error); + this.binding.error.setVisibility(View.VISIBLE); + } + + private static Class findShareViaAccountClass() { + try { + return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity"); + } catch (final ClassNotFoundException e) { + return null; + } + } + + private void handleIntent(final Intent data) { + final String action = data == null ? null : data.getAction(); + if (action == null) { return; } - if (data == null || data.getAction() == null) { - finish(); - return; - } - handled = true; - switch (data.getAction()) { + switch (action) { + case Intent.ACTION_MAIN: + binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE); + break; case Intent.ACTION_VIEW: case Intent.ACTION_SENDTO: - handleUri(data.getData()); + if (handleUri(data.getData())) { + finish(); + } break; case ACTION_SCAN_QR_CODE: - Intent intent = new Intent(this, ScanActivity.class); - startActivityForResult(intent, REQUEST_SCAN_QR_CODE); - return; + Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning()); + setIntent(createMainIntent()); + startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE); + break; } - finish(); + } + private Intent createMainIntent() { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning()); + return intent; } private boolean allowProvisioning() { @@ -213,6 +282,7 @@ public class UriHandlerActivity extends AppCompatActivity { public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { super.onActivityResult(requestCode, requestCode, intent); if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { + final boolean allowProvisioning = allowProvisioning(); final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); if (Strings.isNullOrEmpty(result)) { finish(); @@ -221,22 +291,38 @@ public class UriHandlerActivity extends AppCompatActivity { if (result.startsWith("BEGIN:VCARD\n")) { final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result); if (matcher.find()) { - handleUri(Uri.parse(matcher.group(2)), true); + if (handleUri(Uri.parse(matcher.group(2)), true)) { + finish(); + } + } else { + showError(R.string.no_xmpp_adddress_found); } - finish(); return; - } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning()) { + } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) { ProvisioningUtils.provision(this, result); finish(); return; } - handleUri(Uri.parse(result), true); + final Uri uri = Uri.parse(result.trim()); + if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !Config.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) { + final HttpUrl httpUrl = HttpUrl.parse(uri.toString()); + if (httpUrl != null) { + checkForLinkHeader(httpUrl); + } else { + finish(); + } + } else if (handleUri(uri, true)) { + finish(); + } else { + setIntent(new Intent(Intent.ACTION_VIEW, uri)); + } + } else { + finish(); } - finish(); } private static boolean looksLikeJsonObject(final String input) { - final String trimmed = Strings.emptyToNull(input).trim(); + final String trimmed = Strings.nullToEmpty(input).trim(); return trimmed.charAt(0) == '{' && trimmed.charAt(trimmed.length() - 1) == '}'; } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java deleted file mode 100644 index 26c47f302..000000000 --- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java +++ /dev/null @@ -1,450 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; - -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { - - public static final String ACTION_VERIFY_CONTACT = "verify_contact"; - public static final int MODE_SCAN_FINGERPRINT = -0x0502; - public static final int MODE_ASK_QUESTION = 0x0503; - public static final int MODE_ANSWER_QUESTION = 0x0504; - public static final int MODE_MANUAL_VERIFICATION = 0x0505; - - private LinearLayout mManualVerificationArea; - private LinearLayout mSmpVerificationArea; - private TextView mRemoteFingerprint; - private TextView mYourFingerprint; - private TextView mVerificationExplain; - private TextView mStatusMessage; - private TextView mSharedSecretHint; - private EditText mSharedSecretHintEditable; - private EditText mSharedSecretSecret; - private Button mLeftButton; - private Button mRightButton; - private Account mAccount; - private Conversation mConversation; - private int mode = MODE_MANUAL_VERIFICATION; - private XmppUri mPendingUri = null; - - private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialogInterface, int click) { - mConversation.verifyOtrFingerprint(); - xmppConnectionService.syncRosterToDisk(mConversation.getAccount()); - ToastCompat.makeText(VerifyOTRActivity.this, R.string.verified, Toast.LENGTH_SHORT).show(); - finish(); - } - }; - - private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(final View view) { - if (isAccountOnline()) { - final String question = mSharedSecretHintEditable.getText().toString(); - final String secret = mSharedSecretSecret.getText().toString(); - if (question.trim().isEmpty()) { - mSharedSecretHintEditable.requestFocus(); - mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty)); - } else if (secret.trim().isEmpty()) { - mSharedSecretSecret.requestFocus(); - mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty)); - } else { - mSharedSecretSecret.setError(null); - mSharedSecretHintEditable.setError(null); - initSmp(question, secret); - updateView(); - } - } - } - }; - private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (isAccountOnline()) { - abortSmp(); - updateView(); - } - } - }; - private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() { - - @Override - public void onClick(View view) { - if (isAccountOnline()) { - final String question = mSharedSecretHintEditable.getText().toString(); - final String secret = mSharedSecretSecret.getText().toString(); - respondSmp(question, secret); - updateView(); - } - } - }; - private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - mConversation.smp().hint = null; - mConversation.smp().secret = null; - updateView(); - } - }; - private View.OnClickListener mFinishListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - finish(); - } - }; - - protected boolean initSmp(final String question, final String secret) { - final Session session = mConversation.getOtrSession(); - if (session != null) { - try { - session.initSmp(question, secret); - mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED; - mConversation.smp().secret = secret; - mConversation.smp().hint = question; - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean abortSmp() { - final Session session = mConversation.getOtrSession(); - if (session != null) { - try { - session.abortSmp(); - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - mConversation.smp().hint = null; - mConversation.smp().secret = null; - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean respondSmp(final String question, final String secret) { - final Session session = mConversation.getOtrSession(); - if (session != null) { - try { - session.respondSmp(question, secret); - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean verifyWithUri(XmppUri uri) { - Contact contact = mConversation.getContact(); - if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) { - xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints()); - ToastCompat.makeText(this, R.string.verified, Toast.LENGTH_SHORT).show(); - updateView(); - return true; - } else { - ToastCompat.makeText(this, R.string.could_not_verify_fingerprint, Toast.LENGTH_SHORT).show(); - return false; - } - } - - protected boolean isAccountOnline() { - if (this.mAccount.getStatus() != Account.State.ONLINE) { - ToastCompat.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); - return false; - } else { - return true; - } - } - - protected boolean handleIntent(Intent intent) { - if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) { - this.mAccount = extractAccount(intent); - if (this.mAccount == null) { - return false; - } - try { - this.mConversation = this.xmppConnectionService.find(this.mAccount, Jid.of(intent.getExtras().getString("contact"))); - if (this.mConversation == null) { - return false; - } - } catch (final IllegalArgumentException ignored) { - ignored.printStackTrace(); - return false; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION); - // todo scan OTR fingerprint - if (this.mode == MODE_SCAN_FINGERPRINT) { - Log.d(Config.LOGTAG, "Scan OTR fingerprint is not implemented in this version"); - //new IntentIntegrator(this).initiateScan(); - return false; - } - return true; - } else { - return false; - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - // todo onActivityResult for OTR scan - Log.d(Config.LOGTAG, "Scan OTR fingerprint result is not implemented in this version"); - /*if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); - if (scanResult != null && scanResult.getFormatName() != null) { - String data = scanResult.getContents(); - XmppUri uri = new XmppUri(data); - if (xmppConnectionServiceBound) { - verifyWithUri(uri); - finish(); - } else { - this.mPendingUri = uri; - } - } else { - finish(); - } - }*/ - super.onActivityResult(requestCode, requestCode, intent); - } - - @Override - protected void onBackendConnected() { - if (handleIntent(getIntent())) { - updateView(); - } else if (mPendingUri != null) { - verifyWithUri(mPendingUri); - finish(); - mPendingUri = null; - } - setIntent(null); - } - - protected void updateView() { - if (this.mConversation != null && this.mConversation.hasValidOtrSession()) { - final ActionBar actionBar = getSupportActionBar(); - this.mVerificationExplain.setText(R.string.no_otr_session_found); - invalidateOptionsMenu(); - switch (this.mode) { - case MODE_ASK_QUESTION: - if (actionBar != null) { - actionBar.setTitle(R.string.ask_question); - } - this.updateViewAskQuestion(); - break; - case MODE_ANSWER_QUESTION: - if (actionBar != null) { - actionBar.setTitle(R.string.smp_requested); - } - this.updateViewAnswerQuestion(); - break; - case MODE_MANUAL_VERIFICATION: - default: - if (actionBar != null) { - actionBar.setTitle(R.string.manually_verify); - } - this.updateViewManualVerification(); - break; - } - } else { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.GONE); - } - } - - protected void updateViewManualVerification() { - this.mVerificationExplain.setText(R.string.manual_verification_explanation); - this.mManualVerificationArea.setVisibility(View.VISIBLE); - this.mSmpVerificationArea.setVisibility(View.GONE); - this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); - this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint())); - if (this.mConversation.isOtrFingerprintVerified()) { - deactivateButton(this.mRightButton, R.string.verified); - activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); - } else { - activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); - activateButton(this.mRightButton, R.string.verify, new View.OnClickListener() { - @Override - public void onClick(View view) { - showManuallyVerifyDialog(); - } - }); - } - } - - protected void updateViewAskQuestion() { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.VISIBLE); - this.mVerificationExplain.setText(R.string.smp_explain_question); - final int smpStatus = this.mConversation.smp().status; - switch (smpStatus) { - case Conversation.Smp.STATUS_WE_REQUESTED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint); - this.mSharedSecretSecret.setText(this.mConversation.smp().secret); - this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener); - this.deactivateButton(this.mRightButton, R.string.in_progress); - break; - case Conversation.Smp.STATUS_FAILED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.requestFocus(); - this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); - this.deactivateButton(this.mLeftButton, R.string.cancel); - this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener); - break; - case Conversation.Smp.STATUS_VERIFIED: - this.mSharedSecretHintEditable.setText(""); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretSecret.setText(""); - this.mSharedSecretSecret.setVisibility(View.GONE); - this.mStatusMessage.setVisibility(View.VISIBLE); - this.deactivateButton(this.mLeftButton, R.string.cancel); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - default: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); - this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener); - break; - } - } - - protected void updateViewAnswerQuestion() { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.VISIBLE); - this.mVerificationExplain.setText(R.string.smp_explain_answer); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretHint.setVisibility(View.VISIBLE); - this.deactivateButton(this.mLeftButton, R.string.cancel); - final int smpStatus = this.mConversation.smp().status; - switch (smpStatus) { - case Conversation.Smp.STATUS_CONTACT_REQUESTED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHint.setText(this.mConversation.smp().hint); - this.activateButton(this.mRightButton, R.string.respond, this.mRespondSharedSecretListener); - break; - case Conversation.Smp.STATUS_VERIFIED: - this.mSharedSecretHintEditable.setText(""); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretHint.setVisibility(View.GONE); - this.mSharedSecretSecret.setText(""); - this.mSharedSecretSecret.setVisibility(View.GONE); - this.mStatusMessage.setVisibility(View.VISIBLE); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - case Conversation.Smp.STATUS_FAILED: - default: - this.mSharedSecretSecret.requestFocus(); - this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - } - } - - protected void activateButton(Button button, int text, View.OnClickListener listener) { - button.setEnabled(true); - button.setText(text); - button.setOnClickListener(listener); - } - - protected void deactivateButton(Button button, int text) { - button.setEnabled(false); - button.setText(text); - button.setOnClickListener(null); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_verify_otr); - this.mRemoteFingerprint = findViewById(R.id.remote_fingerprint); - this.mYourFingerprint = findViewById(R.id.your_fingerprint); - this.mLeftButton = findViewById(R.id.left_button); - this.mRightButton = findViewById(R.id.right_button); - this.mVerificationExplain = findViewById(R.id.verification_explanation); - this.mStatusMessage = findViewById(R.id.status_message); - this.mSharedSecretSecret = findViewById(R.id.shared_secret_secret); - this.mSharedSecretHintEditable = findViewById(R.id.shared_secret_hint_editable); - this.mSharedSecretHint = findViewById(R.id.shared_secret_hint); - this.mManualVerificationArea = findViewById(R.id.manual_verification_area); - this.mSmpVerificationArea = findViewById(R.id.smp_verification_area); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.verify_otr, menu); - return true; - } - - private void showManuallyVerifyDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.manually_verify); - builder.setMessage(R.string.are_you_sure_verify_fingerprint); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener); - builder.create().show(); - } - - @Override - protected String getShareableUri() { - if (mAccount != null) { - return mAccount.getShareableUri(); - } else { - return ""; - } - } - - public void onConversationUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - updateView(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java index ccd97bd50..52689b1d8 100644 --- a/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -1,5 +1,9 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.Config.DISALLOW_REGISTRATION_IN_UI; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.readGranted; + import android.Manifest; import android.content.ActivityNotFoundException; import android.content.Intent; @@ -8,12 +12,10 @@ import android.net.Uri; import android.os.Bundle; import android.security.KeyChain; import android.security.KeyChainAliasCallback; -import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; @@ -37,13 +39,8 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import me.drakeet.support.toast.ToastCompat; -import static eu.siacs.conversations.Config.DISALLOW_REGISTRATION_IN_UI; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; - public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback { - private static final int REQUEST_IMPORT_BACKUP = 0x63fb; private static final int REQUEST_READ_EXTERNAL_STORAGE = 0XD737; @@ -106,7 +103,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi } @Override - public void onNewIntent(Intent intent) { + public void onNewIntent(final Intent intent) { super.onNewIntent(intent); if (intent != null) { setIntent(intent); @@ -232,6 +229,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); if (grantResults.length > 0) { if (allGranted(grantResults)) { diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 314acc53b..9c569abb7 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -1,13 +1,9 @@ package eu.siacs.conversations.ui; -import android.Manifest; -import android.util.Pair; -import net.java.otr4j.session.SessionID; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import eu.siacs.conversations.utils.CryptoHelper; +import static eu.siacs.conversations.ui.SettingsActivity.USE_BUNDLED_EMOJIS; +import static eu.siacs.conversations.ui.SettingsActivity.USE_INTERNAL_UPDATER; +import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -91,6 +87,7 @@ import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.BarcodeProvider; import eu.siacs.conversations.services.EmojiService; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.services.UpdateService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.ui.util.AvatarWorkerTask; @@ -109,10 +106,6 @@ import eu.siacs.conversations.xmpp.XmppConnection; import me.drakeet.support.toast.ToastCompat; import pl.droidsonroids.gif.GifDrawable; -import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_OTR_ENCRYPTION; -import static eu.siacs.conversations.ui.SettingsActivity.USE_BUNDLED_EMOJIS; -import static eu.siacs.conversations.ui.SettingsActivity.USE_INTERNAL_UPDATER; - public abstract class XmppActivity extends ActionBarActivity { protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; @@ -426,17 +419,7 @@ public abstract class XmppActivity extends ActionBarActivity { public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { final Contact contact = conversation.getContact(); - if (conversation.hasValidOtrSession()) { - SessionID id = conversation.getOtrSession().getSessionID(); - Jid jid; - try { - jid = Jid.of(id.getAccountID() + "/" + id.getUserID()); - } catch (IllegalArgumentException e) { - jid = null; - } - conversation.setNextCounterpart(jid); - listener.onPresenceSelected(); - } else if (contact.showInRoster() || contact.isSelf()) { + if (contact.showInRoster() || contact.isSelf()) { final Presences presences = contact.getPresences(); if (presences.size() == 0) { if (contact.isSelf()) { @@ -507,8 +490,8 @@ public abstract class XmppActivity extends ActionBarActivity { return getBooleanPreference("unicolored_chatbg", R.bool.use_unicolored_chatbg) || getPreferences().getString(SettingsActivity.THEME, getString(R.string.theme)).equals("black"); } - public boolean enableOTR() { - return getBooleanPreference(ENABLE_OTR_ENCRYPTION, R.bool.enable_otr); + public boolean showDateInQuotes() { + return getBooleanPreference("show_date_in_quotes", R.bool.show_date_in_quotes); } public void setBubbleColor(final View v, final int backgroundColor, final int borderColor) { @@ -788,7 +771,10 @@ public abstract class XmppActivity extends ActionBarActivity { builder.setTitle(contact.getJid().toString()); builder.setMessage(getString(R.string.not_in_roster)); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact, true)); + builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> { + xmppConnectionService.createContact(contact, true); + recreate(); + }); builder.create().show(); } @@ -923,56 +909,6 @@ public abstract class XmppActivity extends ActionBarActivity { } } - private void showPresenceSelectionDialog(Presences presences, final Conversation conversation, final OnPresenceSelected listener) { - final Contact contact = conversation.getContact(); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.choose_presence)); - final String[] resourceArray = presences.toResourceArray(); - Pair, Map> typeAndName = presences.toTypeAndNameMap(); - final Map resourceTypeMap = typeAndName.first; - final Map resourceNameMap = typeAndName.second; - final String[] readableIdentities = new String[resourceArray.length]; - final AtomicInteger selectedResource = new AtomicInteger(0); - for (int i = 0; i < resourceArray.length; ++i) { - String resource = resourceArray[i]; - if (resource.equals(contact.getLastResource())) { - selectedResource.set(i); - } - String type = resourceTypeMap.get(resource); - String name = resourceNameMap.get(resource); - if (type != null) { - if (Collections.frequency(resourceTypeMap.values(), type) == 1) { - readableIdentities[i] = PresenceSelector.translateType(this, type); - } else if (name != null) { - if (Collections.frequency(resourceNameMap.values(), name) == 1 - || CryptoHelper.UUID_PATTERN.matcher(resource).matches()) { - readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + name + ")"; - } else { - readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + name + " / " + resource + ")"; - } - } else { - readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + resource + ")"; - } - } else { - readableIdentities[i] = resource; - } - } - builder.setSingleChoiceItems(readableIdentities, - selectedResource.get(), - (dialog, which) -> selectedResource.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - try { - Jid next = Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()]); - conversation.setNextCounterpart(next); - } catch (IllegalArgumentException e) { - conversation.setNextCounterpart(null); - } - listener.onPresenceSelected(); - }); - builder.create().show(); - } - protected void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { @@ -1036,14 +972,11 @@ public abstract class XmppActivity extends ActionBarActivity { ToastCompat.makeText(this, R.string.no_accounts, ToastCompat.LENGTH_SHORT).show(); return; } - - if (!xmppConnectionService.multipleAccounts()) { - Account mAccount = xmppConnectionService.getAccounts().get(0); - if (EasyOnboardingInvite.hasAccountSupport(mAccount)) { - selectAccountToStartEasyInvite(); - } else { - String user = Jid.ofEscaped(mAccount.getJid()).getLocal(); - String domain = Jid.ofEscaped(mAccount.getJid()).getDomain().toEscapedString(); + if (!selectAccountToStartEasyInvite()) { + if (!xmppConnectionService.multipleAccounts()) { + final Account mAccount = xmppConnectionService.getAccounts().get(0); + final String user = Jid.ofEscaped(mAccount.getJid()).getLocal(); + final String domain = Jid.ofEscaped(mAccount.getJid()).getDomain().toEscapedString(); String inviteURL; try { inviteURL = new getAdHocInviteUri(mAccount.getXmppConnection(), mAccount).execute().get(); @@ -1058,40 +991,36 @@ public abstract class XmppActivity extends ActionBarActivity { inviteURL = Config.inviteUserURL + user + "/" + domain; } Log.d(Config.LOGTAG, "Invite uri = " + inviteURL); - String inviteText = getString(R.string.InviteText, user); - Intent intent = new Intent(android.content.Intent.ACTION_SEND); + final String inviteText = getString(R.string.InviteText, user); + final Intent intent = new Intent(android.content.Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, user + " " + getString(R.string.inviteUser_Subject) + " " + getString(R.string.app_name)); intent.putExtra(Intent.EXTRA_TEXT, inviteText + "\n\n" + inviteURL); startActivity(Intent.createChooser(intent, getString(R.string.invite_contact))); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - } else { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.chooce_account); - final View dialogView = this.getLayoutInflater().inflate(R.layout.choose_account_dialog, null); - final Spinner spinner = dialogView.findViewById(R.id.account); - builder.setView(dialogView); - List mActivatedAccounts = new ArrayList<>(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - if (Config.DOMAIN_LOCK != null) { - mActivatedAccounts.add(account.getJid().getLocal()); - } else { - mActivatedAccounts.add(account.getJid().asBareJid().toString()); + } else { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.chooce_account); + final View dialogView = this.getLayoutInflater().inflate(R.layout.choose_account_dialog, null); + final Spinner spinner = dialogView.findViewById(R.id.account); + builder.setView(dialogView); + List mActivatedAccounts = new ArrayList<>(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.State.DISABLED) { + if (Config.DOMAIN_LOCK != null) { + mActivatedAccounts.add(account.getJid().getLocal()); + } else { + mActivatedAccounts.add(account.getJid().asBareJid().toString()); + } } } - } - StartConversationActivity.populateAccountSpinner(this, mActivatedAccounts, spinner); - builder.setPositiveButton(R.string.ok, - (dialog, id) -> { - String selection = spinner.getSelectedItem().toString(); - Account mAccount = xmppConnectionService.findAccountByJid(Jid.of(selection).asBareJid()); - if (EasyOnboardingInvite.hasAccountSupport(mAccount)) { - selectAccountToStartEasyInvite(); - } else { - String user = Jid.of(mAccount.getJid()).getLocal(); - String domain = Jid.of(mAccount.getJid()).getDomain().toEscapedString(); + StartConversationActivity.populateAccountSpinner(this, mActivatedAccounts, spinner); + builder.setPositiveButton(R.string.ok, + (dialog, id) -> { + final String selection = spinner.getSelectedItem().toString(); + final Account mAccount = xmppConnectionService.findAccountByJid(Jid.of(selection).asBareJid()); + final String user = Jid.of(mAccount.getJid()).getLocal(); + final String domain = Jid.of(mAccount.getJid()).getDomain().toEscapedString(); String inviteURL; try { inviteURL = new getAdHocInviteUri(mAccount.getXmppConnection(), mAccount).execute().get(); @@ -1106,30 +1035,31 @@ public abstract class XmppActivity extends ActionBarActivity { inviteURL = Config.inviteUserURL + user + "/" + domain; } Log.d(Config.LOGTAG, "Invite uri = " + inviteURL); - String inviteText = getString(R.string.InviteText, user); - Intent intent = new Intent(Intent.ACTION_SEND); + final String inviteText = getString(R.string.InviteText, user); + final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, user + " " + getString(R.string.inviteUser_Subject) + " " + getString(R.string.app_name)); intent.putExtra(Intent.EXTRA_TEXT, inviteText + "\n\n" + inviteURL); startActivity(Intent.createChooser(intent, getString(R.string.invite_contact))); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }); - builder.setNegativeButton(R.string.cancel, null); - builder.create().show(); + }); + builder.setNegativeButton(R.string.cancel, null); + builder.create().show(); + } } } - private void selectAccountToStartEasyInvite() { + private boolean selectAccountToStartEasyInvite() { final List accounts = EasyOnboardingInvite.getSupportingAccounts(this.xmppConnectionService); if (accounts.size() == 0) { //This can technically happen if opening the menu item races with accounts reconnecting or something ToastCompat.makeText(this, R.string.no_active_accounts_support_this, ToastCompat.LENGTH_LONG).show(); + return false; } else if (accounts.size() == 1) { openEasyInviteScreen(accounts.get(0)); } else { final AtomicReference selectedAccount = new AtomicReference<>(accounts.get(0)); - final android.app.AlertDialog.Builder alertDialogBuilder = new android.app.AlertDialog.Builder(this); + final androidx.appcompat.app.AlertDialog.Builder alertDialogBuilder = new androidx.appcompat.app.AlertDialog.Builder(this); alertDialogBuilder.setTitle(R.string.choose_account); final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toEscapedString()).toArray(new String[0]); alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which))); @@ -1137,6 +1067,7 @@ public abstract class XmppActivity extends ActionBarActivity { alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> openEasyInviteScreen(selectedAccount.get())); alertDialogBuilder.create().show(); } + return true; } private void openEasyInviteScreen(final Account account) { @@ -1457,7 +1388,44 @@ public abstract class XmppActivity extends ActionBarActivity { return installFromUnknownSource; } - + protected void openInstallFromUnknownSourcesDialogIfNeeded(boolean showToast) { + String ShowToast; + if (showToast == true) { + ShowToast = "true"; + } else { + ShowToast = "false"; + } + if (!installFromUnknownSourceAllowed() && xmppConnectionService.installedFrom() == null) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.install_from_unknown_sources_disabled); + builder.setMessage(R.string.install_from_unknown_sources_disabled_dialog); + builder.setPositiveButton(R.string.next, (dialog, which) -> { + Intent intent; + if (android.os.Build.VERSION.SDK_INT >= 26) { + intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); + Uri uri = Uri.parse("package:" + getPackageName()); + intent.setData(uri); + } else { + intent = new Intent(Settings.ACTION_SECURITY_SETTINGS); + } + Log.d(Config.LOGTAG, "Allow install from unknown sources for Android SDK " + Build.VERSION.SDK_INT + " intent " + intent.toString()); + try { + startActivityForResult(intent, REQUEST_UNKNOWN_SOURCE_OP); + } catch (ActivityNotFoundException e) { + ToastCompat.makeText(XmppActivity.this, R.string.device_does_not_support_unknown_source_op, ToastCompat.LENGTH_SHORT).show(); + } finally { + UpdateService task = new UpdateService(this, xmppConnectionService.installedFrom(), xmppConnectionService); + task.executeOnExecutor(UpdateService.THREAD_POOL_EXECUTOR, ShowToast); + Log.d(Config.LOGTAG, "AppUpdater started"); + } + }); + builder.create().show(); + } else { + UpdateService task = new UpdateService(this, xmppConnectionService.installedFrom(), xmppConnectionService); + task.executeOnExecutor(UpdateService.THREAD_POOL_EXECUTOR, ShowToast); + Log.d(Config.LOGTAG, "AppUpdater started"); + } + } public void ShowAvatarPopup(final Activity activity, final AvatarService.Avatarable user) { final AlertDialog.Builder builder = new AlertDialog.Builder(activity); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 801c1b56f..d6079f213 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -1,10 +1,13 @@ package eu.siacs.conversations.ui.adapter; +import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; + import android.content.SharedPreferences; import android.graphics.Typeface; import android.preference.PreferenceManager; import android.text.Spannable; import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.style.StyleSpan; import android.util.Pair; import android.view.LayoutInflater; @@ -32,15 +35,12 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.IrregularUnicodeDetector; +import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; -import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; - public class ConversationAdapter extends RecyclerView.Adapter { private static final float INACTIVE_ALPHA = 0.4684f; @@ -162,12 +162,12 @@ public class ConversationAdapter extends RecyclerView.Adapter preview = UIHelper.getMessagePreview(activity, message, viewHolder.binding.conversationLastmsg.getCurrentTextColor()); if (showPreviewText) { - if (message.getBody().equals(DELETED_MESSAGE_BODY)) { - viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(activity.getString(R.string.message_deleted)))); - } else if (message.getBody().equals(DELETED_MESSAGE_BODY_OLD)) { + if (message.hasDeletedBody()) { viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(activity.getString(R.string.message_deleted)))); } else { - viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(replaceYoutube(activity.getApplicationContext(), preview.first.toString())))); + SpannableStringBuilder body = new SpannableStringBuilder(replaceYoutube(activity.getApplicationContext(), preview.first.toString())); + StylingHelper.format(body, viewHolder.binding.conversationLastmsg.getCurrentTextColor(), true); + viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(body))); } } else { viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first); @@ -220,8 +220,8 @@ public class ConversationAdapter extends RecyclerView.Adapter view(context, attachment)); + } + + private static void view(final Context context, Attachment attachment) { + final Intent view = new Intent(Intent.ACTION_VIEW); + final Uri uri = FileBackend.getUriForUri(context, attachment.getUri()); + view.setDataAndType(uri, attachment.getMime()); + view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + try { + context.startActivity(view); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); + } catch (final SecurityException e) { + Toast.makeText(context, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show(); + } } public void addMediaPreviews(List attachments) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 2196f762a..ef96587bd 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -1,11 +1,24 @@ package eu.siacs.conversations.ui.adapter; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; +import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; +import static eu.siacs.conversations.persistance.FileBackend.formatTime; +import static eu.siacs.conversations.persistance.FileBackend.safeLongToInt; +import static eu.siacs.conversations.ui.SettingsActivity.PLAY_GIF_INSIDE; +import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE; +import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE; +import static eu.siacs.conversations.ui.util.MyLinkify.removeTrackingParameter; +import static eu.siacs.conversations.ui.util.MyLinkify.removeTrailingBracket; +import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; + import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; @@ -36,7 +49,6 @@ import android.widget.TextView; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import androidx.core.graphics.ColorUtils; import androidx.core.graphics.drawable.DrawableCompat; import com.google.common.base.Strings; @@ -71,6 +83,7 @@ import eu.siacs.conversations.ui.text.DividerSpan; import eu.siacs.conversations.ui.text.QuoteSpan; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.MyLinkify; +import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.ui.util.ViewUtil; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; @@ -90,18 +103,6 @@ import eu.siacs.conversations.xmpp.mam.MamReference; import me.drakeet.support.toast.ToastCompat; import pl.droidsonroids.gif.GifImageView; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; -import static eu.siacs.conversations.persistance.FileBackend.formatTime; -import static eu.siacs.conversations.persistance.FileBackend.safeLongToInt; -import static eu.siacs.conversations.ui.SettingsActivity.PLAY_GIF_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE; -import static eu.siacs.conversations.ui.util.MyLinkify.removeTrackingParameter; -import static eu.siacs.conversations.ui.util.MyLinkify.removeTrailingBracket; -import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; - public class MessageAdapter extends ArrayAdapter { public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; @@ -209,7 +210,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.indicatorReceived.setVisibility(View.GONE); } if (viewHolder.edit_indicator != null) { - if (message.edited()) { + if (message.edited() && message.getRetractId() == null) { viewHolder.edit_indicator.setVisibility(View.VISIBLE); viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); @@ -217,6 +218,15 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.edit_indicator.setVisibility(View.GONE); } } + if (viewHolder.retract_indicator != null) { + if (message.getRetractId() != null) { + viewHolder.retract_indicator.setVisibility(View.VISIBLE); + viewHolder.retract_indicator.setImageResource(darkBackground ? R.drawable.ic_delete_white_18dp : R.drawable.ic_delete_black_18dp); + viewHolder.retract_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); + } else { + viewHolder.retract_indicator.setVisibility(View.GONE); + } + } final Transferable transferable = message.getTransferable(); boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI && message.getMergedStatus() <= Message.STATUS_RECEIVED; @@ -236,7 +246,7 @@ public class MessageAdapter extends ArrayAdapter { case Message.STATUS_UNSEND: if (transferable != null) { info = getContext().getString(R.string.sending); - showProgress(viewHolder, transferable); + showProgress(viewHolder, transferable, message); } else { info = getContext().getString(R.string.sending); } @@ -405,10 +415,10 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.audioPlayer.setVisibility(View.GONE); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(text); - showProgress(viewHolder, message.getTransferable()); + showProgress(viewHolder, message.getTransferable(), message); if (darkBackground) { viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark); } else { @@ -417,16 +427,35 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.messageBody.setTextIsSelectable(false); } - private void showProgress(final ViewHolder viewHolder, final Transferable transferable) { + private void showProgress(final ViewHolder viewHolder, final Transferable transferable, final Message message) { if (transferable != null) { - if (transferable.getStatus() == Transferable.STATUS_DOWNLOADING || transferable.getStatus() == Transferable.STATUS_UPLOADING || transferable.getStatus() == Transferable.STATUS_WAITING) { - viewHolder.progressBar.setVisibility(View.VISIBLE); + if (message.fileIsTransferring()) { + viewHolder.transfer.setVisibility(View.VISIBLE); viewHolder.progressBar.setProgress(transferable.getProgress()); + Drawable icon = activity.getResources().getDrawable(R.drawable.ic_cancel_black_24dp); + Drawable drawable = DrawableCompat.wrap(icon); + DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); + viewHolder.cancel_transfer.setImageDrawable(drawable); + viewHolder.cancel_transfer.setEnabled(true); + viewHolder.cancel_transfer.setOnClickListener(v -> { + try { + if (activity instanceof ConversationsActivity) { + ConversationFragment conversationFragment = ConversationFragment.get(activity); + if (conversationFragment != null) { + activity.invalidateOptionsMenu(); + conversationFragment.cancelTransmission(message); + } + } + } catch (Exception e) { + viewHolder.cancel_transfer.setEnabled(false); + e.printStackTrace(); + } + }); } else { - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); } } else { - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); } } @@ -435,7 +464,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.audioPlayer.setVisibility(View.GONE); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); if (darkBackground) { viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark); @@ -486,7 +515,7 @@ public class MessageAdapter extends ArrayAdapter { }); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.GONE); } @@ -511,48 +540,51 @@ public class MessageAdapter extends ArrayAdapter { */ private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { boolean startsWithQuote = false; - char previous = '\n'; - int lineStart = -1; - int lineTextStart = -1; - int quoteStart = -1; - for (int i = 0; i <= body.length(); i++) { - char current = body.length() > i ? body.charAt(i) : '\n'; - if (lineStart == -1) { - if (previous == '\n') { - if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i)) - || current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) { - // Line start with quote - lineStart = i; - if (quoteStart == -1) quoteStart = i; - if (i == 0) startsWithQuote = true; - } else if (quoteStart >= 0) { - // Line start without quote, apply spans there - applyQuoteSpan(body, quoteStart, i - 1, darkBackground); - quoteStart = -1; + int quoteDepth = 1; + while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + for (int i = 0; i <= body.length(); i++) { + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(body, quoteStart, i - 1, darkBackground); + quoteStart = -1; + } + } + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + body.delete(lineStart, lineTextStart); + i -= lineTextStart - lineStart; + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + lineStart = -1; + lineTextStart = -1; } } - } else { - // Remove extra spaces between > and first character in the line - // > character will be removed too - if (current != ' ' && lineTextStart == -1) { - lineTextStart = i; - } - if (current == '\n') { - body.delete(lineStart, lineTextStart); - i -= lineTextStart - lineStart; - if (i == lineStart) { - // Avoid empty lines because span over empty line can be hidden - body.insert(i++, " "); - } - lineStart = -1; - lineTextStart = -1; - } + previous = current; } - previous = current; - } - if (quoteStart >= 0) { - // Apply spans to finishing open quote - applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + } + quoteDepth++; } return startsWithQuote; } @@ -561,7 +593,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.download_button.setVisibility(View.GONE); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); viewHolder.audioPlayer.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); if (darkBackground) { @@ -587,7 +619,7 @@ public class MessageAdapter extends ArrayAdapter { body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); body.append("\u2026"); } - final Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); + final Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); for (Message.MergeSeparator mergeSeparator : mergeSeparators) { int start = body.getSpanStart(mergeSeparator); int end = body.getSpanEnd(mergeSeparator); @@ -636,7 +668,7 @@ public class MessageAdapter extends ArrayAdapter { body.setSpan(new RelativeSizeSpan(1.5f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } - StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); + StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor(), true); if (highlightedTerm != null) { StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); } @@ -659,7 +691,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.audioPlayer.setVisibility(View.GONE); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.VISIBLE); viewHolder.download_button.setText(text); final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_download_grey600_48dp); @@ -675,7 +707,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.audioPlayer.setVisibility(View.GONE); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); final String mimeType = message.getMimeType(); if (mimeType != null && message.getMimeType().contains("vcard")) { try { @@ -764,7 +796,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.audioPlayer.setVisibility(View.GONE); showImages(false, viewHolder); viewHolder.download_button.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); String url; if (message.isWebUri()) { url = removeTrackingParameter(Uri.parse(message.getBody().trim())).toString(); @@ -786,9 +818,10 @@ public class MessageAdapter extends ArrayAdapter { } else { scaledH = (int) (100 / ((double) 100 / target)); } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(WRAP_CONTENT, scaledH); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT); layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); viewHolder.richlinkview.setLayoutParams(layoutParams); + viewHolder.richlinkview.setMinimumHeight(scaledH); final String weburl; if (link.startsWith("http://") || link.startsWith("https://")) { weburl = removeTrailingBracket(link); @@ -817,7 +850,7 @@ public class MessageAdapter extends ArrayAdapter { final String url = GeoHelper.MapPreviewUri(message, activity); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); if (mShowMapsInside) { showImages(mShowMapsInside, 0, false, viewHolder); final double target = activity.getResources().getDimension(R.dimen.image_preview_width); @@ -858,10 +891,12 @@ public class MessageAdapter extends ArrayAdapter { } private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) { + final Resources res = activity.getResources(); + viewHolder.messageBody.setWidth((int) res.getDimension(R.dimen.audio_player_width)); toggleWhisperInfo(viewHolder, message, showTitle(message), darkBackground); showImages(false, viewHolder); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.GONE); final RelativeLayout audioPlayer = viewHolder.audioPlayer; audioPlayer.setVisibility(View.VISIBLE); @@ -899,9 +934,11 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.download_button.setVisibility(View.GONE); viewHolder.audioPlayer.setVisibility(View.GONE); viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.progressBar.setVisibility(View.GONE); + viewHolder.transfer.setVisibility(View.GONE); final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - if (!file.exists()) { + if (!file.exists() && !message.isFileDeleted()) { + markFileDeleted(message); + displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted), darkBackground, message); ToastCompat.makeText(activity, R.string.file_deleted, ToastCompat.LENGTH_SHORT).show(); return; } @@ -1092,6 +1129,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.resend_button = view.findViewById(R.id.resend_button); viewHolder.indicator = view.findViewById(R.id.security_indicator); viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.retract_indicator = view.findViewById(R.id.retract_indicator); viewHolder.images = view.findViewById(R.id.images); viewHolder.mediaduration = view.findViewById(R.id.media_duration); viewHolder.image = view.findViewById(R.id.message_image); @@ -1101,7 +1139,9 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.user = view.findViewById(R.id.message_user); viewHolder.time = view.findViewById(R.id.message_time); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.transfer = view.findViewById(R.id.transfer); viewHolder.progressBar = view.findViewById(R.id.progressBar); + viewHolder.cancel_transfer = view.findViewById(R.id.cancel_transfer); break; case RECEIVED: view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); @@ -1113,6 +1153,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.answer_button = view.findViewById(R.id.answer); viewHolder.indicator = view.findViewById(R.id.security_indicator); viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.retract_indicator = view.findViewById(R.id.retract_indicator); viewHolder.images = view.findViewById(R.id.images); viewHolder.mediaduration = view.findViewById(R.id.media_duration); viewHolder.image = view.findViewById(R.id.message_image); @@ -1123,7 +1164,9 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.time = view.findViewById(R.id.message_time); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); viewHolder.encryption = view.findViewById(R.id.message_encryption); + viewHolder.transfer = view.findViewById(R.id.transfer); viewHolder.progressBar = view.findViewById(R.id.progressBar); + viewHolder.cancel_transfer = view.findViewById(R.id.cancel_transfer); break; case STATUS: view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); @@ -1153,9 +1196,6 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); } viewHolder.message_box.setBackgroundResource(darkBackground ? R.drawable.date_bubble_dark : R.drawable.date_bubble); - - int date_bubble_color = ColorUtils.setAlphaComponent(StyledAttributes.getColor(activity, R.attr.colorAccent), 80); //set alpha to date_bubble - activity.setBubbleColor(viewHolder.message_box, (date_bubble_color), -1); // themed color return view; } else if (type == RTP_SESSION) { final boolean isDarkTheme = activity.isDarkTheme(); @@ -1179,9 +1219,6 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful, isDarkTheme)); viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f); viewHolder.message_box.setBackgroundResource(darkBackground ? R.drawable.date_bubble_dark : R.drawable.date_bubble); - - int date_bubble_color = ColorUtils.setAlphaComponent(StyledAttributes.getColor(activity, R.attr.colorAccent), 80); //set alpha to date bubble - activity.setBubbleColor(viewHolder.message_box, (date_bubble_color), -1); //themed color return view; } else if (type == STATUS) { if ("LOAD_MORE".equals(message.getBody())) { @@ -1240,6 +1277,10 @@ public class MessageAdapter extends ArrayAdapter { } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground); } else { + if (!activity.xmppConnectionService.getFileBackend().getFile(message).exists() && !message.isFileDeleted()) { + markFileDeleted(message); + displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted), darkBackground, message); + } if (checkFileExistence(message, view, viewHolder)) { markFileExisting(message); } else { @@ -1282,12 +1323,12 @@ public class MessageAdapter extends ArrayAdapter { } else if (message.treatAsDownloadable()) { try { final URI uri = new URI(message.getBody()); - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize_on_host, - UIHelper.getFileDescriptionString(activity, message), - uri.getHost()), - darkBackground); + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + uri.getHost()), + darkBackground); } catch (Exception e) { displayDownloadableMessage(viewHolder, message, @@ -1348,10 +1389,21 @@ public class MessageAdapter extends ArrayAdapter { } private void markFileExisting(Message message) { - Log.d(Config.LOGTAG, "Found and restored orphaned file " + message.getRelativeFilePath()); - message.setFileDeleted(false); - activity.xmppConnectionService.updateMessage(message, false); - activity.xmppConnectionService.updateConversation((Conversation) message.getConversation()); + new Thread(() -> { + Log.d(Config.LOGTAG, "Found and restored orphaned file " + message.getRelativeFilePath()); + message.setFileDeleted(false); + activity.xmppConnectionService.updateMessage(message, false); + activity.xmppConnectionService.updateConversation((Conversation) message.getConversation()); + }).start(); + } + + private void markFileDeleted(Message message) { + new Thread(() -> { + Log.d(Config.LOGTAG, "Mark file deleted " + message.getRelativeFilePath()); + message.setFileDeleted(true); + activity.xmppConnectionService.updateMessage(message, false); + activity.xmppConnectionService.updateConversation((Conversation) message.getConversation()); + }).start(); } private boolean checkFileExistence(Message message, View view, ViewHolder viewHolder) { @@ -1436,6 +1488,7 @@ public class MessageAdapter extends ArrayAdapter { public Button load_more_messages; public ImageView edit_indicator; + public ImageView retract_indicator; public RelativeLayout audioPlayer; public RelativeLayout images; protected LinearLayout message_box; @@ -1455,7 +1508,9 @@ public class MessageAdapter extends ArrayAdapter { protected ImageView contact_picture; protected TextView status_message; protected TextView encryption; + protected RelativeLayout transfer; protected ProgressBar progressBar; + protected ImageButton cancel_transfer; } public void setBubbleBackgroundColor(final View viewHolder, final int type, @@ -1490,4 +1545,4 @@ public class MessageAdapter extends ArrayAdapter { } } } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageLogAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageLogAdapter.java new file mode 100644 index 000000000..b1d5e67f5 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageLogAdapter.java @@ -0,0 +1,97 @@ +package eu.siacs.conversations.ui.adapter; + +import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; +import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Date; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.ui.adapter.model.MessageLogModel; + +public class MessageLogAdapter extends ArrayAdapter implements View.OnClickListener { + + private final ArrayList dataSet; + Context mContext; + + // View lookup cache + private static class ViewHolder { + TextView txtLineNr; + TextView txtBody; + TextView txtTimeSent; + } + + public MessageLogAdapter(ArrayList data, Context context) { + super(context, R.layout.message_log_item, data); + this.dataSet = data; + this.mContext = context; + } + + @Override + public void onClick(View v) { + + } + + private int lastPosition = -1; + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // Get the data item for this position + MessageLogModel dataModel = getItem(position); + // Check if an existing view is being reused, otherwise inflate the view + ViewHolder viewHolder; // view lookup cache stored in tag + + final View result; + + if (convertView == null) { + + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(getContext()); + convertView = inflater.inflate(R.layout.message_log_item, parent, false); + viewHolder.txtLineNr = convertView.findViewById(R.id.nr); + viewHolder.txtBody = convertView.findViewById(R.id.body); + viewHolder.txtTimeSent = convertView.findViewById(R.id.timeSent); + + result = convertView; + + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + result = convertView; + } + + Animation animation = AnimationUtils.loadAnimation(mContext, (position > lastPosition) ? R.anim.ufb : R.anim.dft); + result.startAnimation(animation); + lastPosition = position; + viewHolder.txtLineNr.setText(String.valueOf(position + 1)); + viewHolder.txtBody.setText(preview(dataModel)); + viewHolder.txtTimeSent.setText(getTimeSentFormated(dataModel.getTimeSent())); + // Return the completed view to render on screen + return convertView; + } + + private String getTimeSentFormated(long timeSent) { + return android.text.format.DateFormat.getDateFormat(getContext()).format(new Date(timeSent)) + " " + android.text.format.DateFormat.getTimeFormat(getContext()).format(new Date(timeSent)); + } + + private String preview(MessageLogModel dataModel) { + if (hasDeletedBody(dataModel.getBody())) { + return getContext().getString(R.string.message_deleted); + } else { + return dataModel.getBody(); + } + } + + public boolean hasDeletedBody(String message) { + return message.trim().equals(DELETED_MESSAGE_BODY) || message.trim().equals(DELETED_MESSAGE_BODY_OLD); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java index 4df5f0278..edcbdbf65 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java @@ -67,7 +67,7 @@ public class UserAdapter extends ListAdapter { final XmppActivity activity = XmppActivity.find(v); if (activity != null) { @@ -115,8 +115,6 @@ public class UserAdapter extends ListAdapter T reflectiveRead(@NonNull Object object, @NonNull String fieldName) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(object); + } catch (final Exception ex) { + return null; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index 552b071cf..0c5b57aee 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -46,7 +46,6 @@ import java.util.List; import java.util.UUID; import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.MimeUtils; public class Attachment implements Parcelable { @@ -138,10 +137,10 @@ public class Attachment implements Parcelable { return Collections.singletonList(new Attachment(uri, type, mime)); } - public static List of(final Context context, List uris) { - List attachments = new ArrayList<>(); - for (Uri uri : uris) { - final String mime = MimeUtils.guessMimeTypeFromUri(context, uri); + public static List of(final Context context, List uris, final String type) { + final List attachments = new ArrayList<>(); + for (final Uri uri : uris) { + final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type); attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); } return attachments; @@ -182,7 +181,7 @@ public class Attachment implements Parcelable { private static boolean renderFileThumbnail(final String mime) { return mime.startsWith("video/") || isImage(mime) - || (Compatibility.runsTwentyOne() && "application/pdf".equals(mime)); + || "application/pdf".equals(mime); } public Uri getUri() { diff --git a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java b/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java index fb3706773..59a94a508 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java @@ -132,7 +132,6 @@ public class ConversationMenuConfigurator { if (conversation.getNextEncryption() != Message.ENCRYPTION_NONE) { menuSecure.setIcon(R.drawable.ic_lock_white_24dp); } - otr.setVisible(Config.supportOtr() && activity.enableOTR()); if (conversation.getMode() == Conversation.MODE_MULTI) { otr.setVisible(false); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/CustomTab.java b/src/main/java/eu/siacs/conversations/ui/util/CustomTab.java index 864d496d6..fa083dc94 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/CustomTab.java +++ b/src/main/java/eu/siacs/conversations/ui/util/CustomTab.java @@ -11,8 +11,6 @@ import androidx.browser.customtabs.CustomTabsIntent; import eu.siacs.conversations.R; -import static eu.siacs.conversations.utils.Compatibility.runsTwentyOne; - public class CustomTab { public static void openTab(Context context, Uri uri, boolean dark) throws ActivityNotFoundException { CustomTabsIntent.Builder tabBuilder = new CustomTabsIntent.Builder(); @@ -25,9 +23,7 @@ public class CustomTab { tabBuilder.setColorScheme(dark ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT); tabBuilder.setShareState(CustomTabsIntent.SHARE_STATE_ON); CustomTabsIntent customTabsIntent = tabBuilder.build(); - if (runsTwentyOne()) { - customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - } + customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); customTabsIntent.intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); customTabsIntent.launchUrl(context, uri); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/GridManager.java b/src/main/java/eu/siacs/conversations/ui/util/GridManager.java index 2f71d75ab..bc369b364 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/GridManager.java +++ b/src/main/java/eu/siacs/conversations/ui/util/GridManager.java @@ -50,10 +50,7 @@ public class GridManager { private static ColumnInfo calculateColumnCount(Context context, int availableWidth, @DimenRes int desiredSize) { final float desiredWidth = context.getResources().getDimension(desiredSize); - int columns = Math.round(availableWidth / desiredWidth); - if (columns < 1) { - columns = 1; - } + final int columns = Math.round(availableWidth / desiredWidth); final int realWidth = availableWidth / columns; Log.d(Config.LOGTAG, "desired=" + desiredWidth + " real=" + realWidth); return new ColumnInfo(columns, realWidth); diff --git a/src/main/java/eu/siacs/conversations/ui/util/KeyboardUtils.java b/src/main/java/eu/siacs/conversations/ui/util/KeyboardUtils.java new file mode 100644 index 000000000..6c4859ad0 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/KeyboardUtils.java @@ -0,0 +1,121 @@ +package eu.siacs.conversations.ui.util; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; + +import java.util.HashMap; + +/** + * Based on the following Stackoverflow answer: + * http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android + * https://github.com/ravindu1024/android-keyboardlistener/tree/master/keyboard-listener/src/main/java/com/rw/keyboardlistener + */ +@SuppressWarnings("WeakerAccess") +public class KeyboardUtils implements ViewTreeObserver.OnGlobalLayoutListener { + private final static int MAGIC_NUMBER = 200; + + private SoftKeyboardToggleListener mCallback; + private View mRootView; + private Boolean prevValue = null; + private float mScreenDensity; + private static HashMap sListenerMap = new HashMap<>(); + + public interface SoftKeyboardToggleListener { + void onToggleSoftKeyboard(boolean isVisible); + } + + + @Override + public void onGlobalLayout() { + Rect r = new Rect(); + mRootView.getWindowVisibleDisplayFrame(r); + + int heightDiff = mRootView.getRootView().getHeight() - (r.bottom - r.top); + float dp = heightDiff / mScreenDensity; + boolean isVisible = dp > MAGIC_NUMBER; + + if (mCallback != null && (prevValue == null || isVisible != prevValue)) { + prevValue = isVisible; + mCallback.onToggleSoftKeyboard(isVisible); + } + } + + /** + * Add a new keyboard listener + * + * @param act calling activity + * @param listener callback + */ + public static void addKeyboardToggleListener(Activity act, SoftKeyboardToggleListener listener) { + removeKeyboardToggleListener(listener); + + sListenerMap.put(listener, new KeyboardUtils(act, listener)); + } + + /** + * Remove a registered listener + * + * @param listener {@link SoftKeyboardToggleListener} + */ + public static void removeKeyboardToggleListener(SoftKeyboardToggleListener listener) { + if (sListenerMap.containsKey(listener)) { + KeyboardUtils k = sListenerMap.get(listener); + k.removeListener(); + + sListenerMap.remove(listener); + } + } + + /** + * Remove all registered keyboard listeners + */ + public static void removeAllKeyboardToggleListeners() { + for (SoftKeyboardToggleListener l : sListenerMap.keySet()) + sListenerMap.get(l).removeListener(); + + sListenerMap.clear(); + } + + /** + * Manually toggle soft keyboard visibility + * + * @param context calling context + */ + public static void toggleKeyboardVisibility(Context context) { + InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + + /** + * Force closes the soft keyboard + * + * @param activeView the view with the keyboard focus + */ + public static void forceCloseKeyboard(View activeView) { + InputMethodManager inputMethodManager = (InputMethodManager) activeView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) + inputMethodManager.hideSoftInputFromWindow(activeView.getWindowToken(), 0); + } + + private void removeListener() { + mCallback = null; + + mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + + private KeyboardUtils(Activity act, SoftKeyboardToggleListener listener) { + mCallback = listener; + + mRootView = ((ViewGroup) act.findViewById(android.R.id.content)).getChildAt(0); + mRootView.getViewTreeObserver().addOnGlobalLayoutListener(this); + + mScreenDensity = act.getResources().getDisplayMetrics().density; + } +} + diff --git a/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java b/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java new file mode 100644 index 000000000..7164e0769 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java @@ -0,0 +1,69 @@ +package eu.siacs.conversations.ui.util; + +import android.location.Location; + +import org.osmdroid.util.GeoPoint; + +import eu.siacs.conversations.Config; + +public final class LocationHelper { + /** + * Parses a lat long string in the form "lat,long". + * + * @param latlong A string in the form "lat,long" + * @return A GeoPoint representing the lat,long string. + * @throws NumberFormatException If an invalid lat or long is specified. + */ + public static GeoPoint parseLatLong(final String latlong) throws NumberFormatException { + if (latlong == null || latlong.isEmpty()) { + return null; + } + + final String[] parts = latlong.split(","); + if (parts[1].contains("?")) { + parts[1] = parts[1].substring(0, parts[1].indexOf("?")); + } + return new GeoPoint(Double.valueOf(parts[0]), Double.valueOf(parts[1])); + } + + private static boolean isSameProvider(final String provider1, final String provider2) { + if (provider1 == null) { + return provider2 == null; + } + return provider1.equals(provider2); + } + + public static boolean isBetterLocation(final Location location, final Location prevLoc) { + if (prevLoc == null) { + return true; + } + + // Check whether the new location fix is newer or older + final long timeDelta = location.getTime() - prevLoc.getTime(); + final boolean isSignificantlyNewer = timeDelta > Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; + final boolean isSignificantlyOlder = timeDelta < -Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; + final boolean isNewer = timeDelta > 0; + + if (isSignificantlyNewer) { + return true; + } else if (isSignificantlyOlder) { + return false; + } + + // Check whether the new location fix is more or less accurate + final int accuracyDelta = (int) (location.getAccuracy() - prevLoc.getAccuracy()); + final boolean isLessAccurate = accuracyDelta > 0; + final boolean isMoreAccurate = accuracyDelta < 0; + final boolean isSignificantlyLessAccurate = accuracyDelta > 200; + + // Check if the old and new location are from the same provider + final boolean isFromSameProvider = isSameProvider(location.getProvider(), prevLoc.getProvider()); + + // Determine location quality using a combination of timeliness and accuracy + if (isMoreAccurate) { + return true; + } else if (isNewer && !isLessAccurate) { + return true; + } else return isNewer && !isSignificantlyLessAccurate && isFromSameProvider; + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index 81fa348b0..dc5d97ec0 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -32,18 +32,22 @@ package eu.siacs.conversations.ui.util; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; -import android.os.Build; import android.preference.PreferenceManager; import android.text.Editable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.util.Linkify; +import android.util.Base64; import android.util.Log; import android.webkit.URLUtil; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,12 +57,32 @@ import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.Patterns; -import eu.siacs.conversations.utils.TrackingHelper; import eu.siacs.conversations.utils.XmppUri; public class MyLinkify { - private static final Pattern youtubePattern = Pattern.compile("(www\\.|m\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)\\/(((?!(\"|'|<)).)*)"); + private final static Pattern youtubePattern = Pattern.compile("(www\\.|m\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)/(((?!([\"'<])).)*)"); + private final static String youtubeURLPattern = "(?:youtube(?:-nocookie)?\\.com\\/(?:[^\\/\\n\\s]+\\/\\S+\\/|(?:v|e(?:mbed)?)\\/|\\S*?[?&]v=)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})"; + + public static boolean isYoutubeUrl(String url) { + return !url.isEmpty() && url.matches("(?i:http|https):\\/\\/" + youtubePattern); + } + + public static String getYoutubeVideoId(String url) { + if (url == null || url.trim().length() <= 0) { + return null; + } + final Pattern pattern = Pattern.compile(youtubeURLPattern, Pattern.CASE_INSENSITIVE); + final Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + public static String getYoutubeImageUrl(String url) { + return "https://img.youtube.com/vi/" + getYoutubeVideoId(url) + "/0.jpg"; + } public static String replaceYoutube(Context context, String content) { return replaceYoutube(context, new SpannableStringBuilder(content)).toString(); @@ -69,30 +93,53 @@ public class MyLinkify { if (useInvidious(context)) { while (matcher.find()) { final String youtubeId = matcher.group(3); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - if (matcher.group(2) != null && Objects.equals(matcher.group(2), "youtu.be")) { - content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true"))); - content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true"))); - } else { - content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/" + youtubeId + "&local=true"))); - content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/" + youtubeId + "&local=true"))); - } + if (matcher.group(2) != null && matcher.group(2).equals("youtu.be")) { + content = new SpannableStringBuilder(content.toString().replaceAll("https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true"))); } else { - if (matcher.group(2) != null && matcher.group(2) == "youtu.be") { - content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true"))); - content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true"))); - } else { - content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/" + youtubeId + "&local=true"))); - content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/" + youtubeId + "&local=true"))); - } + content = new SpannableStringBuilder(content.toString().replaceAll("https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/" + youtubeId + "&local=true"))); } } } return content; } - // https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/AdapterMessage.java + // https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java + // https://github.com/newhouse/url-tracking-stripper + private static final List PARANOID_QUERY = Collections.unmodifiableList(Arrays.asList( + // https://en.wikipedia.org/wiki/UTM_parameters + "awt_a", // AWeber + "awt_l", // AWeber + "awt_m", // AWeber + + "icid", // Adobe + "gclid", // Google + "gclsrc", // Google ads + "dclid", // DoubleClick (Google) + "fbclid", // Facebook + "igshid", // Instagram + + "mc_cid", // MailChimp + "mc_eid", // MailChimp + + "zanpid", // Zanox (Awin) + + "kclickid" // https://support.freespee.com/hc/en-us/articles/202577831-Kenshoo-integration + )); + + // https://github.com/snarfed/granary/blob/master/granary/facebook.py#L1789 + + private static final List FACEBOOK_WHITELIST_PATH = Collections.unmodifiableList(Arrays.asList( + "/nd/", "/n/", "/story.php" + )); + + private static final List FACEBOOK_WHITELIST_QUERY = Collections.unmodifiableList(Arrays.asList( + "story_fbid", "fbid", "id", "comment_id" + )); + public static SpannableString removeTrackingParameter(Uri uri) { + if (uri.isOpaque()) + return new SpannableString(uri.toString()); + boolean changed = false; Uri url; Uri.Builder builder; @@ -101,6 +148,12 @@ public class MyLinkify { !TextUtils.isEmpty(uri.getQueryParameter("url"))) { changed = true; url = Uri.parse(uri.getQueryParameter("url")); + } else if ("https".equals(uri.getScheme()) && + "smex-ctp.trendmicro.com".equals(uri.getHost()) && + "/wis/clicktime/v1/query".equals(uri.getPath()) && + !TextUtils.isEmpty(uri.getQueryParameter("url"))) { + changed = true; + url = Uri.parse(uri.getQueryParameter("url")); } else if ("https".equals(uri.getScheme()) && "www.google.com".equals(uri.getHost()) && uri.getPath() != null && @@ -121,26 +174,53 @@ public class MyLinkify { } changed = (result != null); url = (result == null ? uri : result); - } else { + } else if (uri.getQueryParameterNames().size() == 1) { + // Sophos Email Appliance + Uri result = null; + String key = uri.getQueryParameterNames().iterator().next(); + if (TextUtils.isEmpty(uri.getQueryParameter(key))) + try { + String data = new String(Base64.decode(key, Base64.DEFAULT)); + int v = data.indexOf("ver="); + int u = data.indexOf("&&url="); + if (v == 0 && u > 0) + result = Uri.parse(URLDecoder.decode(data.substring(u + 6), StandardCharsets.UTF_8.name())); + } catch (Throwable ex) { + ex.printStackTrace(); + } + changed = (result != null); + url = (result == null ? uri : result); + } else url = uri; - } if (url.isOpaque()) { return new SpannableString(uri.toString()); - //return uri; } builder = url.buildUpon(); builder.clearQuery(); - for (String key : url.getQueryParameterNames()) + String host = uri.getHost(); + String path = uri.getPath(); + if (host != null) + host = host.toLowerCase(Locale.ROOT); + if (path != null) + path = path.toLowerCase(Locale.ROOT); + boolean first = "www.facebook.com".equals(host); + for (String key : url.getQueryParameterNames()) { // https://en.wikipedia.org/wiki/UTM_parameters // https://docs.oracle.com/en/cloud/saas/marketing/eloqua-user/Help/EloquaAsynchronousTrackingScripts/EloquaTrackingParameters.htm - if (key.toLowerCase(Locale.ROOT).startsWith("utm_") || - key.toLowerCase(Locale.ROOT).startsWith("elq") || - TrackingHelper.TRACKING_PARAMETER.contains(key.toLowerCase(Locale.ROOT)) || - ("snr".equals(key) && "store.steampowered.com".equals(uri.getHost()))) + String lkey = key.toLowerCase(Locale.ROOT); + if (PARANOID_QUERY.contains(lkey) || + lkey.startsWith("utm_") || + lkey.startsWith("elq") || + ((host != null && host.endsWith("facebook.com")) && + !first && + FACEBOOK_WHITELIST_PATH.contains(path) && + !FACEBOOK_WHITELIST_QUERY.contains(lkey)) || + ("store.steampowered.com".equals(host) && + "snr".equals(lkey))) changed = true; else if (!TextUtils.isEmpty(key)) for (String value : url.getQueryParameters(key)) { - Log.i(Config.LOGTAG, "Query " + key + "=" + value); + Log.d(Config.LOGTAG, "Query " + key + "=" + value); Uri suri = Uri.parse(value); if ("http".equals(suri.getScheme()) || "https".equals(suri.getScheme())) { Uri s = Uri.parse(removeTrackingParameter(suri).toString()); @@ -151,6 +231,8 @@ public class MyLinkify { } builder.appendQueryParameter(key, value); } + first = false; + } return (changed ? new SpannableString(builder.build().toString()) : new SpannableString(uri.toString())); } @@ -202,16 +284,13 @@ public class MyLinkify { return false; } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - if (end < cs.length()) { - // Reject strings that were probably matched only because they contain a dot followed by - // by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java) - if (isAlphabetic(cs.charAt(end - 1)) && isAlphabetic(cs.charAt(end))) { - return false; - } + if (end < cs.length()) { + // Reject strings that were probably matched only because they contain a dot followed by + // by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java) + if (isAlphabetic(cs.charAt(end - 1)) && isAlphabetic(cs.charAt(end))) { + return false; } } - return true; }; @@ -221,20 +300,7 @@ public class MyLinkify { }; private static boolean isAlphabetic(final int code) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return Character.isAlphabetic(code); - } - switch (Character.getType(code)) { - case Character.UPPERCASE_LETTER: - case Character.LOWERCASE_LETTER: - case Character.TITLECASE_LETTER: - case Character.MODIFIER_LETTER: - case Character.OTHER_LETTER: - case Character.LETTER_NUMBER: - return true; - default: - return false; - } + return Character.isAlphabetic(code); } private static String invidiousHost(Context context) { @@ -248,8 +314,7 @@ public class MyLinkify { private static boolean useInvidious(Context context) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean invidious = sharedPreferences.getBoolean(SettingsActivity.USE_INVIDIOUS, context.getResources().getBoolean(R.bool.use_invidious)); - return invidious; + return sharedPreferences.getBoolean(SettingsActivity.USE_INVIDIOUS, context.getResources().getBoolean(R.bool.use_invidious)); } public static void addLinks(Editable body, boolean includeGeo) { diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java index d363e5502..c98eafdd2 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java @@ -1,51 +1,114 @@ package eu.siacs.conversations.ui.util; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.UIHelper; public class QuoteHelper { - public static boolean isPositionQuoteCharacter(CharSequence body, int pos){ - return body.charAt(pos) == '>'; + + public static final char QUOTE_CHAR = '>'; + public static final char QUOTE_END_CHAR = '<'; // used for one check, not for actual quoting + public static final char QUOTE_ALT_CHAR = '»'; + public static final char QUOTE_ALT_END_CHAR = '«'; + + public static boolean isPositionQuoteCharacter(CharSequence body, int pos) { + // second part of logical check actually goes against the logic indicated in the method name, since it also checks for context + // but it's very useful + return body.charAt(pos) == QUOTE_CHAR || isPositionAltQuoteStart(body, pos); + } + + public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos) { + return body.charAt(pos) == QUOTE_END_CHAR; + } + + public static boolean isPositionAltQuoteCharacter(CharSequence body, int pos) { + return body.charAt(pos) == QUOTE_ALT_CHAR; + } + + public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) { + return body.charAt(pos) == QUOTE_ALT_END_CHAR; + } + + public static boolean isPositionAltQuoteStart(CharSequence body, int pos) { + return isPositionAltQuoteCharacter(body, pos) && !isPositionFollowedByAltQuoteEnd(body, pos); } public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) { - return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos +1 ); + return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1); } // 'Prequote' means anything we require or can accept in front of a QuoteChar - public static boolean isPositionPrecededByPrequote(CharSequence body, int pos){ + public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) { return UIHelper.isPositionPrecededByLineStart(body, pos); } - public static boolean isPositionQuoteStart (CharSequence body, int pos){ - return isPositionQuoteCharacter(body, pos) - && isPositionPrecededByPrequote(body, pos) - && (UIHelper.isPositionFollowedByWhitespace(body, pos) - || isPositionFollowedByQuoteChar(body, pos)); + public static boolean isPositionQuoteStart(CharSequence body, int pos) { + return (isPositionQuoteCharacter(body, pos) + && isPositionPrecededByPreQuote(body, pos) + && (UIHelper.isPositionFollowedByQuoteableCharacter(body, pos) + || isPositionFollowedByQuoteChar(body, pos))); } - public static boolean bodyContainsQuoteStart (CharSequence body){ - for (int i = 0; i < body.length(); i++){ - if (isPositionQuoteStart(body, i)){ + public static boolean bodyContainsQuoteStart(CharSequence body) { + for (int i = 0; i < body.length(); i++) { + if (isPositionQuoteStart(body, i)) { return true; } } return false; } - /*public static int getQuoteColors(XmppActivity activity, boolean darkBackground, int quoteDepth){ - int[] colorsLight = R.style.ConversationsTheme_Dark; - int[] colorsDark = Config.QUOTE_COLOR_ARRAY_DARK; - Collections.rotate(Collections.singletonList(colorsLight), quoteDepth); - Collections.rotate(Collections.singletonList(colorsDark), quoteDepth); + public static boolean isPositionFollowedByAltQuoteEnd(CharSequence body, int pos) { + if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) { + return false; + } + boolean previousWasWhitespace = false; + for (int i = pos + 1; i < body.length(); i++) { + char c = body.charAt(i); + if (c == '\n' || isPositionAltQuoteCharacter(body, i)) { + return false; + } else if (isPositionAltQuoteEndCharacter(body, i) && !previousWasWhitespace) { + return true; + } else { + previousWasWhitespace = Character.isWhitespace(c); + } + } + return false; + } - Arrays.stream(colorsLight).toArray(); + public static boolean isNestedTooDeeply(CharSequence line) { + if (isPositionQuoteStart(line, 0)) { + int nestingDepth = 1; + for (int i = 1; i < line.length(); i++) { + if (isPositionQuoteStart(line, i)) { + nestingDepth++; + } + if (nestingDepth > (Config.QUOTING_MAX_DEPTH - 1)) { + return true; + } + } + } + return false; + } - int quoteColors = darkBackground ? ContextCompat.getColor(activity, colorsLight[quoteDepth-1]) - : ContextCompat.getColor(activity, colorsDark[quoteDepth-1]); + public static String replaceAltQuoteCharsInText(String text) { + for (int i = 0; i < text.length(); i++) { + if (isPositionAltQuoteStart(text, i)) { + text = text.substring(0, i) + QUOTE_CHAR + text.substring(i + 1); + } + } + return text; + } - Collections.rotate - - return quoteColors; - };*/ -} + public static boolean isMessageQuoteable(Message m){ + if (m.isTypeText() && MessageUtils.prepareQuote(m).length() > 0) { + return true; + } + if (m.isFileOrImage()){ + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/Rationals.java b/src/main/java/eu/siacs/conversations/ui/util/Rationals.java index bc469a013..31155cd6e 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Rationals.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Rationals.java @@ -23,4 +23,4 @@ public final class Rationals { return input; } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index a7fcb0351..3ed27f312 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -97,10 +97,10 @@ public class ShareUtil { url = message.getBody(); } else if (message.hasFileOnRemoteHost()) { resId = R.string.file_url; - url = message.getFileParams().url; + url = message.getFileParams().url; } else { - final Message.FileParams fileParams = message.getFileParams(); - url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim(); + final Message.FileParams fileParams = message.getFileParams(); + url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim(); resId = R.string.file_url; } if (activity.copyTextToClipboard(url, resId)) { diff --git a/src/main/java/eu/siacs/conversations/ui/util/UpdateHelper.java b/src/main/java/eu/siacs/conversations/ui/util/UpdateHelper.java index 05d25e3d3..3fa9c10ae 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/UpdateHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/UpdateHelper.java @@ -28,12 +28,12 @@ import me.drakeet.support.toast.ToastCompat; public class UpdateHelper { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); private static final String INSTALL_DATE = "2020-11-01"; - private static final String monocles_message = "MONOCLES_CHAT_UPDATE_MESSAGE"; + private static final String monocles_message = "MONOCLES.IM_UPDATE_MESSAGE"; private static boolean moveData = true; private static boolean dataMoved = false; private static final File PAM_MainDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/"); - private static final File Monocles_MainDirectory = new File(Environment.getExternalStorageDirectory() + "/monocles chat/"); + private static final File monocles_MainDirectory = new File(Environment.getExternalStorageDirectory() + "/monocles chat/"); private static final File PAM_PicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Images/"); private static final File PAM_FilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Files/"); private static final File PAM_AudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Audios/"); @@ -115,7 +115,7 @@ public class UpdateHelper { private static void checkOldData() { if (PAM_MainDirectory.exists() && PAM_MainDirectory.isDirectory()) { - if (Monocles_MainDirectory.exists() && Monocles_MainDirectory.isDirectory()) { + if (monocles_MainDirectory.exists() && monocles_MainDirectory.isDirectory()) { moveData = false; } else { moveData = true; @@ -180,16 +180,16 @@ public class UpdateHelper { } } if (PAM_MainDirectory.exists() && PAM_MainDirectory.isDirectory()) { - Monocles_MainDirectory.mkdirs(); + monocles_MainDirectory.mkdirs(); final File[] files = PAM_MainDirectory.listFiles(); if (files == null) { return; } - if (PAM_MainDirectory.renameTo(Monocles_MainDirectory)) { + if (PAM_MainDirectory.renameTo(monocles_MainDirectory)) { dataMoved = true; - Log.d(Config.LOGTAG, "moved " + PAM_MainDirectory.getAbsolutePath() + " to " + Monocles_MainDirectory.getAbsolutePath()); + Log.d(Config.LOGTAG, "moved " + PAM_MainDirectory.getAbsolutePath() + " to " + monocles_MainDirectory.getAbsolutePath()); } else { - Log.d(Config.LOGTAG, "could not move " + PAM_MainDirectory.getAbsolutePath() + " to " + Monocles_MainDirectory.getAbsolutePath()); + Log.d(Config.LOGTAG, "could not move " + PAM_MainDirectory.getAbsolutePath() + " to " + monocles_MainDirectory.getAbsolutePath()); } } } @@ -265,7 +265,7 @@ public class UpdateHelper { private static boolean PAMInstalled(Activity activity) { PackageManager pm = activity.getPackageManager(); try { - return pm.getApplicationLabel(pm.getApplicationInfo("de.monocles.chat", 0)).equals("Monocles Messenger"); + return pm.getApplicationLabel(pm.getApplicationInfo("de.pixart.messenger", 0)).equals("Monocles Messenger"); } catch (PackageManager.NameNotFoundException e) { return false; } diff --git a/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java b/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java new file mode 100644 index 000000000..2022cdf59 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java @@ -0,0 +1,30 @@ +package eu.siacs.conversations.ui.util; + +import java.util.HashMap; + +/** + * Helper methods for parsing URI's. + */ +public final class UriHelper { + /** + * Parses a query string into a hashmap. + * + * @param q The query string to split. + * @return A hashmap containing the key-value pairs from the query string. + */ + public static HashMap parseQueryString(final String q) { + if (q == null || q.isEmpty()) { + return null; + } + + final String[] query = q.split("&"); + // TODO: Look up the HashMap implementation and figure out what the load factor is and make sure we're not reallocating here. + final HashMap queryMap = new HashMap<>(query.length); + for (final String param : query) { + final String[] pair = param.split("="); + queryMap.put(pair[0], pair.length == 2 && !pair[1].isEmpty() ? pair[1] : null); + } + + return queryMap; + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java index 4b42a2c9f..7f0795d33 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java @@ -24,6 +24,7 @@ import java.util.concurrent.Executors; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.ui.util.QuoteHelper; public class EditMessage extends EmojiWrapperEditText { @@ -141,7 +142,8 @@ public class EditMessage extends EmojiWrapperEditText { } public void insertAsQuote(String text) { - text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", ""); + text = QuoteHelper.replaceAltQuoteCharsInText(text); + text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2").replaceAll("(^|\n)([^" + QuoteHelper.QUOTE_CHAR + "])", "$1> $2").replaceAll("\n$", ""); Editable editable = getEditableText(); int position = getSelectionEnd(); if (position == -1) position = editable.length(); diff --git a/src/main/java/eu/siacs/conversations/ui/widget/Marker.java b/src/main/java/eu/siacs/conversations/ui/widget/Marker.java new file mode 100644 index 000000000..9cd7919b9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/Marker.java @@ -0,0 +1,54 @@ +package eu.siacs.conversations.ui.widget; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Point; + +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; + +/** + * An immutable marker overlay. + */ +public class Marker extends SimpleLocationOverlay { + private final GeoPoint position; + private final Bitmap icon; + private final Point mapPoint; + + /** + * Create a marker overlay which will be drawn at the current Geographical position. + * + * @param icon A bitmap icon for the marker + * @param position The geographic position where the marker will be drawn (if it is inside the view) + */ + public Marker(final Bitmap icon, final GeoPoint position) { + super(icon); + this.icon = icon; + this.position = position; + this.mapPoint = new Point(); + } + + /** + * Create a marker overlay which will be drawn centered in the view. + * + * @param icon A bitmap icon for the marker + */ + public Marker(final Bitmap icon) { + this(icon, null); + } + + @Override + public void draw(final Canvas c, final MapView view, final boolean shadow) { + super.draw(c, view, shadow); + + // If no position was set for the marker, draw it centered in the view. + view.getProjection().toPixels(this.position == null ? view.getMapCenter() : position, mapPoint); + + c.drawBitmap(icon, + mapPoint.x - icon.getWidth() / 2, + mapPoint.y - icon.getHeight(), + null); + + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java b/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java new file mode 100644 index 000000000..02ea0dab4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java @@ -0,0 +1,52 @@ +package eu.siacs.conversations.ui.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.location.Location; + +import org.osmdroid.util.GeoPoint; +import org.osmdroid.util.TileSystem; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.ui.util.StyledAttributes; + +public class MyLocation extends SimpleLocationOverlay { + private final GeoPoint position; + private final float accuracy; + private final Point mapCenterPoint; + private final Paint fill; + private final Paint outline; + + public MyLocation(final Context ctx, final Bitmap icon, final Location position) { + super(icon); + this.mapCenterPoint = new Point(); + this.fill = new Paint(Paint.ANTI_ALIAS_FLAG); + final int accent = StyledAttributes.getColor(ctx, R.attr.colorAccent); + fill.setColor(accent); + fill.setStyle(Paint.Style.FILL); + this.outline = new Paint(Paint.ANTI_ALIAS_FLAG); + outline.setColor(accent); + outline.setAlpha(50); + outline.setStyle(Paint.Style.FILL); + this.position = new GeoPoint(position); + this.accuracy = position.getAccuracy(); + } + + @Override + public void draw(final Canvas c, final MapView view, final boolean shadow) { + super.draw(c, view, shadow); + + view.getProjection().toPixels(position, mapCenterPoint); + c.drawCircle(mapCenterPoint.x, mapCenterPoint.y, + Math.max(Config.Map.MY_LOCATION_INDICATOR_SIZE + Config.Map.MY_LOCATION_INDICATOR_OUTLINE_SIZE, + accuracy / (float) TileSystem.GroundResolution(position.getLatitude(), view.getZoomLevel()) + ), this.outline); + c.drawCircle(mapCenterPoint.x, mapCenterPoint.y, Config.Map.MY_LOCATION_INDICATOR_SIZE, this.fill); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/RichLinkView.java b/src/main/java/eu/siacs/conversations/ui/widget/RichLinkView.java index 458176bec..17563937f 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/RichLinkView.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/RichLinkView.java @@ -9,13 +9,14 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; -import java.util.HashMap; -import java.util.Map; import androidx.annotation.RequiresApi; import com.squareup.picasso.Picasso; +import java.util.HashMap; +import java.util.Map; + import eu.siacs.conversations.R; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.util.CustomTab; @@ -174,7 +175,6 @@ public class RichLinkView extends RelativeLayout { private static Map linkMap = new HashMap<>(); - public void setLink(final String url, final String filename, final boolean dataSaverDisabled, final XmppConnectionService mXmppConnectionService, final int color, final RichPreview.ViewListener viewListener) { MetaData data = linkMap.get(url); if (data == null) { @@ -188,8 +188,8 @@ public class RichLinkView extends RelativeLayout { linkMap.put(url, metaData); } initView(dataSaverDisabled, color); - } + @Override public void onError(Exception e) { viewListener.onError(e); diff --git a/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java b/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java index 818cd60ef..06f604076 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java @@ -45,4 +45,4 @@ public class SurfaceViewRenderer extends org.webrtc.SurfaceViewRenderer { public interface OnAspectRatioChanged { void onAspectRatioChanged(final Rational rational); } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 75f2e7eac..c1d44b7d5 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; + import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; @@ -25,8 +27,6 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.SettingsFragment; -import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; - public class Compatibility { private static final List UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList( SettingsActivity.SHOW_FOREGROUND_SERVICE, @@ -47,14 +47,6 @@ public class Compatibility { return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); } - public static boolean runsTwentyOne() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } - - public static boolean runsNineTeen() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - } - public static boolean runsTwentySix() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } @@ -94,6 +86,7 @@ public class Compatibility { return true; //when in doubt… } } + private static boolean targetsThirty(Context context) { try { final PackageManager packageManager = context.getPackageManager(); @@ -117,6 +110,7 @@ public class Compatibility { return true; //when in doubt… } } + public static boolean runsAndTargetsTwentyFour(Context context) { return runsTwentyFour() && targetsTwentyFour(context); } @@ -124,7 +118,6 @@ public class Compatibility { public static boolean runsAndTargetsTwentySix(Context context) { return runsTwentySix() && targetsTwentySix(context); } - public static boolean runsAndTargetsThirty(Context context) { return runsThirty() && targetsThirty(context); } @@ -180,14 +173,9 @@ public class Compatibility { } } - @SuppressLint("UnsupportedChromeOsCameraSystemFeature") public static boolean hasFeatureCamera(final Context context) { final PackageManager packageManager = context.getPackageManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); - } else { - return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA); - } + return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java b/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java index 55043ca42..fa43da14a 100644 --- a/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java +++ b/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java @@ -16,9 +16,9 @@ import eu.siacs.conversations.xmpp.XmppConnection; public class EasyOnboardingInvite implements Parcelable { - private String domain; - private String uri; - private String landingUrl; + private final String domain; + private final String uri; + private final String landingUrl; protected EasyOnboardingInvite(Parcel in) { domain = in.readString(); @@ -61,7 +61,6 @@ public class EasyOnboardingInvite implements Parcelable { return false; } return getSupportingAccounts(service).size() > 0; - } public static List getSupportingAccounts(final XmppConnectionService service) { @@ -88,6 +87,9 @@ public class EasyOnboardingInvite implements Parcelable { return Strings.isNullOrEmpty(landingUrl) ? uri : landingUrl; } + public String getShareableUri() { + return uri; + } public String getDomain() { return domain; diff --git a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java index 4124964ea..e69de29bb 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.siacs.conversations.utils; - -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; - -public class ExifHelper { - private static final String TAG = "CameraExif"; - - public static int getOrientation(InputStream is) { - if (is == null) { - return 0; - } - - byte[] buf = new byte[8]; - int length = 0; - - // ISO/IEC 10918-1:1993(E) - while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) { - int marker = buf[1] & 0xFF; - - // Check if the marker is a padding. - if (marker == 0xFF) { - continue; - } - - // Check if the marker is SOI or TEM. - if (marker == 0xD8 || marker == 0x01) { - continue; - } - // Check if the marker is EOI or SOS. - if (marker == 0xD9 || marker == 0xDA) { - return 0; - } - - // Get the length and check if it is reasonable. - if (!read(is, buf, 2)) { - return 0; - } - length = pack(buf, 0, 2, false); - if (length < 2) { - Log.e(TAG, "Invalid length"); - return 0; - } - length -= 2; - - // Break if the marker is EXIF in APP1. - if (marker == 0xE1 && length >= 6) { - if (!read(is, buf, 6)) return 0; - length -= 6; - if (pack(buf, 0, 4, false) == 0x45786966 && - pack(buf, 4, 2, false) == 0) { - break; - } - } - - // Skip other markers. - try { - is.skip(length); - } catch (IOException ex) { - return 0; - } - length = 0; - } - - // JEITA CP-3451 Exif Version 2.2 - if (length > 8) { - int offset = 0; - byte[] jpeg = new byte[length]; - if (!read(is, jpeg, length)) { - return 0; - } - - // Identify the byte order. - int tag = pack(jpeg, offset, 4, false); - if (tag != 0x49492A00 && tag != 0x4D4D002A) { - Log.e(TAG, "Invalid byte order"); - return 0; - } - boolean littleEndian = (tag == 0x49492A00); - - // Get the offset and check if it is reasonable. - int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; - if (count < 10 || count > length) { - Log.e(TAG, "Invalid offset"); - return 0; - } - offset += count; - length -= count; - - // Get the count and go through all the elements. - count = pack(jpeg, offset - 2, 2, littleEndian); - while (count-- > 0 && length >= 12) { - // Get the tag and check if it is orientation. - tag = pack(jpeg, offset, 2, littleEndian); - if (tag == 0x0112) { - // We do not really care about type and count, do we? - int orientation = pack(jpeg, offset + 8, 2, littleEndian); - switch (orientation) { - case 1: - return 0; - case 3: - return 180; - case 6: - return 90; - case 8: - return 270; - } - Log.i(TAG, "Unsupported orientation"); - return 0; - } - offset += 12; - length -= 12; - } - } - - Log.i(TAG, "Orientation not found"); - return 0; - } - - private static int pack(byte[] bytes, int offset, int length, - boolean littleEndian) { - int step = 1; - if (littleEndian) { - offset += length - 1; - step = -1; - } - - int value = 0; - while (length-- > 0) { - value = (value << 8) | (bytes[offset] & 0xFF); - offset += step; - } - return value; - } - - private static boolean read(InputStream is, byte[] buf, int length) { - try { - return is.read(buf, 0, length) == length; - } catch (IOException ex) { - return false; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java index 2f8b72cb5..7c0cf2f1f 100644 --- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java @@ -49,7 +49,7 @@ public class GeoHelper { } catch (NumberFormatException nfe) { return null; } - return getMappreviewHost(activity) + "?center=" + latitude + "," + longitude + "&size=500x500&markers=" + latitude + "," + longitude + "&zoom=" + Config.DEFAULT_ZOOM; + return getMappreviewHost(activity) + "?center=" + latitude + "," + longitude + "&size=500x500&markers=" + latitude + "," + longitude + "&zoom=" + Config.Map.FINAL_ZOOM_LEVEL; } private static String getMappreviewHost(Activity activity) { @@ -72,7 +72,7 @@ public class GeoHelper { try { return URLUtil.isValidUrl(urlstring) && Patterns.WEB_URL.matcher(urlstring).matches(); } catch (Exception e) { - Log.d(Config.LOGTAG, "Could not use custom mappreview host and using monocles chat for mappreview " + e); + Log.d(Config.LOGTAG, "Could not use custom mappreview host and using blabber.im for mappreview " + e); } return false; } @@ -110,14 +110,14 @@ public class GeoHelper { final Conversational conversation = message.getConversation(); final String label = getLabel(context, message); - Intent locationPluginIntent = new Intent(context, ShowLocationActivity.class); + final Intent locationPluginIntent = new Intent(context, ShowLocationActivity.class); locationPluginIntent.putExtra("latitude", geoPoint.getLatitude()); locationPluginIntent.putExtra("longitude", geoPoint.getLongitude()); if (message.getStatus() != Message.STATUS_RECEIVED) { locationPluginIntent.putExtra("jid", conversation.getAccount().getJid().toString()); locationPluginIntent.putExtra("name", context.getString(R.string.me)); } else { - Contact contact = message.getContact(); + final Contact contact = message.getContact(); if (contact != null) { locationPluginIntent.putExtra("name", contact.getDisplayName()); locationPluginIntent.putExtra("jid", contact.getJid().toString()); diff --git a/src/main/java/eu/siacs/conversations/utils/ImStyleParser.java b/src/main/java/eu/siacs/conversations/utils/ImStyleParser.java index 8be18e62a..a3da29a05 100644 --- a/src/main/java/eu/siacs/conversations/utils/ImStyleParser.java +++ b/src/main/java/eu/siacs/conversations/utils/ImStyleParser.java @@ -98,7 +98,8 @@ public class ImStyleParser { return i; } } else if (c == '\n') { - return -1; + // ignore line breaks + //return -1; } } return -1; diff --git a/src/main/java/eu/siacs/conversations/utils/LocationHelper.java b/src/main/java/eu/siacs/conversations/utils/LocationHelper.java index 7cce7910c..babe118e1 100644 --- a/src/main/java/eu/siacs/conversations/utils/LocationHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/LocationHelper.java @@ -19,8 +19,8 @@ public final class LocationHelper { // Check whether the new location fix is newer or older final long timeDelta = location.getTime() - prevLoc.getTime(); - final boolean isSignificantlyNewer = timeDelta > Config.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; - final boolean isSignificantlyOlder = timeDelta < -Config.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; + final boolean isSignificantlyNewer = timeDelta > Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; + final boolean isSignificantlyOlder = timeDelta < -Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; final boolean isNewer = timeDelta > 0; if (isSignificantlyNewer) { diff --git a/src/main/java/eu/siacs/conversations/utils/LocationProvider.java b/src/main/java/eu/siacs/conversations/utils/LocationProvider.java new file mode 100644 index 000000000..3eb786e39 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/LocationProvider.java @@ -0,0 +1,75 @@ +package eu.siacs.conversations.utils; + +import android.content.Context; +import android.telephony.TelephonyManager; +import android.util.Log; + +import androidx.core.content.ContextCompat; + +import org.osmdroid.util.GeoPoint; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Locale; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; + +public class LocationProvider { + + public static final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0); + + public static String getUserCountry(final Context context) { + try { + final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class); + if (tm == null) { + return getUserCountryFallback(); + } + final String simCountry = tm.getSimCountryIso(); + if (simCountry != null && simCountry.length() == 2) { // SIM country code is available + return simCountry.toUpperCase(Locale.US); + } else if (tm.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { // device is not 3G (would be unreliable) + String networkCountry = tm.getNetworkCountryIso(); + if (networkCountry != null && networkCountry.length() == 2) { // network country code is available + return networkCountry.toUpperCase(Locale.US); + } + } + return getUserCountryFallback(); + } catch (final Exception e) { + return getUserCountryFallback(); + } + } + + private static String getUserCountryFallback() { + final Locale locale = Locale.getDefault(); + return locale.getCountry(); + } + + public static GeoPoint getGeoPoint(final Context context) { + return getGeoPoint(context, getUserCountry(context)); + } + + + public static synchronized GeoPoint getGeoPoint(final Context context, final String country) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)))) { + String line; + while ((line = reader.readLine()) != null) { + final String[] parts = line.split("\\s+", 4); + if (parts.length == 4) { + if (country.equalsIgnoreCase(parts[0])) { + try { + return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2])); + } catch (final NumberFormatException e) { + return FALLBACK; + } + } + } + } + } catch (final IOException e) { + Log.d(Config.LOGTAG, "unable to parse country->geo map", e); + } + return FALLBACK; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 9201cc07d..1d431eb5a 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -39,6 +39,7 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.AesGcmURL; import eu.siacs.conversations.http.URL; +import eu.siacs.conversations.ui.util.QuoteHelper; public class MessageUtils { @@ -68,8 +69,7 @@ public class MessageUtils { continue; } final char c = line.charAt(0); - if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) - || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { + if (QuoteHelper.isNestedTooDeeply(line)) { continue; } if (builder.length() != 0) { @@ -81,15 +81,15 @@ public class MessageUtils { } public static boolean treatAsDownloadable(final String body, final boolean oob) { - final String[] lines = body.split("\n"); - if (lines.length == 0) { + final String[] lines = body.split("\n"); + if (lines.length == 0) { + return false; + } + for (final String line : lines) { + if (line.contains("\\s+")) { return false; } - for (final String line : lines) { - if (line.contains("\\s+")) { - return false; - } - } + } final URI uri; try { uri = new URI(lines[0]); @@ -102,11 +102,11 @@ public class MessageUtils { final String ref = uri.getFragment(); final String protocol = uri.getScheme(); final boolean encrypted = ref != null && AesGcmURL.IV_KEY.matcher(ref).matches(); - final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); + final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); final boolean validAesGcm = AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri); final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol); - final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1; - return validAesGcm || validOob; + final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1; + return validAesGcm || validOob; } public static String filterLtrRtl(String body) { @@ -118,6 +118,6 @@ public class MessageUtils { } public static boolean fileWithKnownSize(Message message) { - return message.getType() == Message.TYPE_TEXT && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null; + return message.getType() == Message.TYPE_TEXT && message.isOOb() && message.getFileParams().size != null && message.getFileParams().size > 0 && message.getFileParams().url != null; } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 7ff3abab0..4d4eb4956 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -21,15 +21,20 @@ import android.net.Uri; import android.provider.OpenableColumns; import android.util.Log; +import com.google.common.base.Strings; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.ExportBackupService; @@ -275,6 +280,8 @@ public final class MimeUtils { add("image/ico", "ico"); add("image/ief", "ief"); add("image/heic", "heic"); + add("image/heif", "heif"); + add("image/avif", "avif"); // add ".jpg" first so it will be the default for guessExtensionFromMimeType add("image/jpeg", "jpg"); add("image/jpeg", "jpeg"); @@ -402,6 +409,16 @@ public final class MimeUtils { applyOverrides(); } + private static final List DOCUMENT_MIMES = Arrays.asList( + "application/pdf", + "application/vnd.oasis.opendocument.text", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "text/x-tex", + "text/plain" + ); + + private static void add(String mimeType, String extension) { // If we have an existing x -> y mapping, we do not want to // override it with another mapping x -> y2. @@ -590,22 +607,63 @@ public final class MimeUtils { } public static String extractRelevantExtension(final String path, final boolean ignoreCryptoExtension) { - if (path == null || path.isEmpty()) { + if (Strings.isNullOrEmpty(path)) { return null; } - String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase(); - int dotPosition = filename.lastIndexOf("."); + final String filenameQueryAnchor = path.substring(path.lastIndexOf('/') + 1); + final String filenameQuery = cutBefore(filenameQueryAnchor, '#'); + final String filename = cutBefore(filenameQuery, '?'); + final int dotPosition = filename.lastIndexOf('.'); - if (dotPosition != -1) { - String extension = filename.substring(dotPosition + 1); - // we want the real file extension, not the crypto one - if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) { - return extractRelevantExtension(filename.substring(0, dotPosition)); - } else { - return extension; - } + if (dotPosition == -1) { + return null; } - return null; + final String extension = filename.substring(dotPosition + 1); + // we want the real file extension, not the crypto one + if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) { + return extractRelevantExtension(filename.substring(0, dotPosition)); + } else { + return extension; + } + } + + private static String cutBefore(final String input, final char c) { + final int position = input.indexOf(c); + if (position > 0) { + return input.substring(0, position); + } else { + return input; + } + } + + public static String getMimeTypeEmoji(Context context, String mime){ + String lm; + if (mime == null) { + lm = context.getString(R.string.unknown); + } else if (mime.startsWith("audio/")) { + lm = "\uD83C\uDF99"; // studio microphone emoji + } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) { + lm = "\uD83D\uDCC6"; // tear-off calendar emoji + } else if (mime.equals("text/x-vcard")) { + lm = "\uD83D\uDC64"; // silhouette emoji + } else if (mime.equals("application/vnd.android.package-archive")) { + lm = "\uD83D\uDCF1"; // cell phone emoji + } else if (mime.equals("application/zip") || mime.equals("application/rar")) { + lm = "\uD83D\uDDC4️"; // filing cabinet emoji + } else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) { + lm = "\uD83D\uDCD6"; // open book emoji + } else if (mime.equals(ExportBackupService.MIME_TYPE)) { + lm = "\uD83D\uDCBE"; // diskette emoji + } else if (DOCUMENT_MIMES.contains(mime)) { + lm = "\uD83D\uDCC4"; // page emoji + } else if (mime.startsWith("image/")) { + lm = "\uD83D\uDDBC️"; // painting emoji + } else if (mime.startsWith("video/")) { + lm = "\uD83C\uDFAC"; // clapper board emoji + } else { + lm = "\uD83D\uDCC4"; // page emoji + } + return lm; } } diff --git a/src/main/java/eu/siacs/conversations/utils/Namespace.java b/src/main/java/eu/siacs/conversations/utils/Namespace.java index ca70fd008..055dfa7d7 100644 --- a/src/main/java/eu/siacs/conversations/utils/Namespace.java +++ b/src/main/java/eu/siacs/conversations/utils/Namespace.java @@ -28,6 +28,7 @@ public final class Namespace { public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; + public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java index 3629c9641..b8df60fc8 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/eu/siacs/conversations/utils/Patterns.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; * Commonly used regular expression patterns. */ public class Patterns { + public static final Pattern XMPP_PATTERN = Pattern .compile("xmpp\\:(?:(?:[" + Patterns.GOOD_IRI_CHAR @@ -117,36 +118,6 @@ public class Patterns { + "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)" + "|y[et]" + "|z[amw]))"; - - public static final Pattern EMAIL_ADDRESS - = Pattern.compile( - "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + - "\\@" + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + - "(" + - "\\." + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + - ")+" - ); - /** - * This pattern is intended for searching for things that look like they - * might be phone numbers in arbitrary text, not for validating whether - * something is in fact a phone number. It will miss many things that - * are legitimate phone numbers. - *

- *

The pattern matches the following: - *

    - *
  • Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes - * may follow. - *
  • Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes. - *
  • A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes. - *
- */ - public static final Pattern PHONE - = Pattern.compile( // sdd = space, dot, or dash - "(\\+[0-9]+[\\- \\.]*)?" // +* - + "(\\([0-9]+\\)[\\- \\.]*)?" // ()* - + "([0-9][0-9\\- \\.]+[0-9])"); // + /** * Regular expression to match all IANA top-level domains. *

@@ -295,7 +266,7 @@ public class Patterns { * IPv4-Embedded IPv6 Address (section 2 of rfc6052) * IPv4-mapped IPv6 addresses (section 2.1 of rfc2765) * IPv4-translated addresses (section 2.1 of rfc2765) - * + *

* Taken from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses/17871737#17871737 */ public static final Pattern IP6_ADDRESS @@ -361,7 +332,7 @@ public class Patterns { private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" + ")"; private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD; public static final Pattern DOMAIN_NAME - = Pattern.compile("(" + HOST_NAME + "|" + IP6_ADDRESS + "|" + IP_ADDRESS +")"); + = Pattern.compile("(" + HOST_NAME + "|" + IP6_ADDRESS + "|" + IP_ADDRESS + ")"); private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/"; /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */ private static final String WORD_BOUNDARY = "(?:\\b|$|^)"; @@ -405,7 +376,7 @@ public class Patterns { * Regular expression that matches domain names without a TLD */ private static final String RELAXED_DOMAIN_NAME = - "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")"; + "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" + "?)+" + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")"; /** * Regular expression to match strings that do not start with a supported protocol. The TLDs * are expected to be one of the known TLDs. @@ -468,12 +439,35 @@ public class Patterns { "(?:" + EMAIL_ADDRESS_LOCAL_PART + "@" + EMAIL_ADDRESS_DOMAIN + ")" + WORD_BOUNDARY + ")" ); - + public static final Pattern EMAIL_ADDRESS + = Pattern.compile( + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" + ); /** - * Do not create this static utility class. + * This pattern is intended for searching for things that look like they + * might be phone numbers in arbitrary text, not for validating whether + * something is in fact a phone number. It will miss many things that + * are legitimate phone numbers. + * + *

The pattern matches the following: + *

    + *
  • Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes + * may follow. + *
  • Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes. + *
  • A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes. + *
*/ - private Patterns() { - } + public static final Pattern PHONE + = Pattern.compile( // sdd = space, dot, or dash + "(\\+[0-9]+[\\- \\.]*)?" // +* + + "(\\([0-9]+\\)[\\- \\.]*)?" // ()* + + "([0-9][0-9\\- \\.]+[0-9])"); // + /** * Convenience method to take all of the non-null matching groups in a @@ -516,4 +510,10 @@ public class Patterns { } return buffer.toString(); } -} \ No newline at end of file + + /** + * Do not create this static utility class. + */ + private Patterns() { + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index f9ddbe343..879b3b83a 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -37,6 +37,20 @@ public class PhoneHelper { return uri == null ? null : Uri.parse(uri); } + public static String getVersionName(Context context) { + final String packageName = context == null ? null : context.getPackageName(); + if (packageName != null) { + try { + return context.getPackageManager().getPackageInfo(packageName, 0).versionName; + } catch (final PackageManager.NameNotFoundException e) { + return "unknown"; + } catch (final RuntimeException e) { + return "unknown"; + } + } else { + return "unknown"; + } + } public static String getOSVersion(Context context) { return "Android/" + android.os.Build.MODEL + "/" + android.os.Build.VERSION.RELEASE; diff --git a/src/main/java/eu/siacs/conversations/utils/RichPreview.java b/src/main/java/eu/siacs/conversations/utils/RichPreview.java index ad9af78be..7e0aec70d 100644 --- a/src/main/java/eu/siacs/conversations/utils/RichPreview.java +++ b/src/main/java/eu/siacs/conversations/utils/RichPreview.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.utils; +import static eu.siacs.conversations.ui.util.MyLinkify.getYoutubeImageUrl; +import static eu.siacs.conversations.ui.util.MyLinkify.isYoutubeUrl; + import android.content.Context; import android.os.AsyncTask; import android.view.View; @@ -23,7 +26,6 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.regex.Pattern; -import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.services.XmppConnectionService; @@ -82,7 +84,11 @@ public class RichPreview { metaData.setUrl(json.getString("url")); } if (json.has("imageurl")) { - metaData.setImageurl(json.getString("imageurl")); + if (isYoutubeUrl(metaData.getUrl())) { + metaData.setImageurl(getYoutubeImageUrl(metaData.getUrl())); + } else { + metaData.setImageurl(json.getString("imageurl")); + } } if (json.has("title")) { metaData.setTitle(json.getString("title")); @@ -184,7 +190,7 @@ public class RichPreview { try { doc = Jsoup.connect(url) .timeout(Config.CONNECT_TIMEOUT * 1000) - .userAgent(HttpConnectionManager.getUserAgent()) + .userAgent(xmppConnectionService.getIqGenerator().getUserAgent()) .get(); } catch (Exception e) { e.printStackTrace(); diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 53a653d5f..e74beb942 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import com.google.common.io.ByteStreams; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -13,12 +15,13 @@ import eu.siacs.conversations.Config; public class SocksSocketFactory { public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException { + //TODO use different Socks Addr Type if destination is IP or IPv6 final InputStream proxyIs = socket.getInputStream(); final OutputStream proxyOs = socket.getOutputStream(); proxyOs.write(new byte[]{0x05, 0x01, 0x00}); proxyOs.flush(); final byte[] handshake = new byte[2]; - proxyIs.read(handshake); + ByteStreams.readFully(proxyIs, handshake); if (handshake[0] != 0x05 || handshake[1] != 0x00) { throw new SocksConnectionException("Socks 5 handshake failed"); } @@ -30,19 +33,50 @@ public class SocksSocketFactory { request.putShort((short) port); proxyOs.write(request.array()); proxyOs.flush(); - final byte[] response = new byte[7 + dest.length]; - proxyIs.read(response); - if (response[1] != 0x00) { - if (response[1] == 0x04) { + final byte[] response = new byte[4]; + ByteStreams.readFully(proxyIs, response); + final byte ver = response[0]; + if (ver != 0x05) { + throw new IOException(String.format("Unknown Socks version %02X ", ver)); + } + final byte status = response[1]; + final byte bndAddrType = response[3]; + final byte[] bndDestination = readDestination(bndAddrType, proxyIs); + final byte[] bndPort = new byte[2]; + if (bndAddrType == 0x03) { + final String receivedDestination = new String(bndDestination); + if (!receivedDestination.equalsIgnoreCase(destination)) { + throw new IOException(String.format("Destination mismatch. Received %s Expected %s", receivedDestination, destination)); + } + } + ByteStreams.readFully(proxyIs, bndPort); + if (status != 0x00) { + if (status == 0x04) { throw new HostNotFoundException("Host unreachable"); } - if (response[1] == 0x05) { + if (status == 0x05) { throw new HostNotFoundException("Connection refused"); } - throw new SocksConnectionException("Unable to connect to destination " + (int) (response[1])); + throw new IOException(String.format("Unknown status code %02X ", status)); } } + private static byte[] readDestination(final byte type, final InputStream inputStream) throws IOException { + final byte[] bndDestination; + if (type == 0x01) { + bndDestination = new byte[4]; + } else if (type == 0x03) { + final int length = inputStream.read(); + bndDestination = new byte[length]; + } else if (type == 0x04) { + bndDestination = new byte[16]; + } else { + throw new IOException(String.format("Unknown Socks address type %02X ", type)); + } + ByteStreams.readFully(inputStream, bndDestination); + return bndDestination; + } + public static boolean contains(byte needle, byte[] haystack) { for (byte hay : haystack) { if (hay == needle) { diff --git a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java index 3f54fc13c..6f89bcbc3 100644 --- a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java @@ -40,6 +40,7 @@ import android.text.Spanned; import android.text.TextWatcher; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; @@ -75,23 +76,28 @@ public class StylingHelper { } } - public static void format(final Editable editable, int start, int end, @ColorInt int textColor) { + public static void format(final Editable editable, int start, int end, @ColorInt int textColor, boolean hideStylingKeywords) { for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) { final int keywordLength = style.getKeyword().length(); + final float size = 0f; editable.setSpan(createSpanForStyle(style), style.getStart() + keywordLength, style.getEnd() - keywordLength + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (hideStylingKeywords) { + editable.setSpan(new RelativeSizeSpan(size), style.getStart(), style.getStart() + keywordLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(new RelativeSizeSpan(size), style.getEnd() - keywordLength + 1, style.getEnd() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLength, textColor); makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor); } } - public static void format(final Editable editable, @ColorInt int textColor) { + public static void format(final Editable editable, @ColorInt int textColor, boolean hideStylingKeywords) { int end = 0; Message.MergeSeparator[] spans = editable.getSpans(0, editable.length() - 1, Message.MergeSeparator.class); for (Message.MergeSeparator span : spans) { - format(editable, end, editable.getSpanStart(span), textColor); + format(editable, end, editable.getSpanStart(span), textColor, hideStylingKeywords); end = editable.getSpanEnd(span); } - format(editable, end, editable.length() - 1, textColor); + format(editable, end, editable.length() - 1, textColor, hideStylingKeywords); } public static void highlight(final Context context, final Editable editable, List needles, boolean dark) { @@ -249,7 +255,7 @@ public class StylingHelper { @Override public void afterTextChanged(Editable editable) { clear(editable); - format(editable, mEditText.getCurrentTextColor()); + format(editable, mEditText.getCurrentTextColor(), false); } } } diff --git a/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java b/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java index 348962fd0..b9de1ad0b 100644 --- a/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java @@ -72,10 +72,14 @@ public class TimeFrameUtils { public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) { final long passed = (since < 0) ? 0 : (to - since); - final int hours = (int) (passed / 3600000); - final int minutes = (int) (passed / 60000) % 60; - final int seconds = (int) (passed / 1000) % 60; - final int milliseconds = (int) (passed / 100) % 10; + return formatElapsedTime(passed, withMilliseconds); + } + + public static String formatElapsedTime(final long elapsed, final boolean withMilliseconds) { + final int hours = (int) (elapsed / 3600000); + final int minutes = (int) (elapsed / 60000) % 60; + final int seconds = (int) (elapsed / 1000) % 60; + final int milliseconds = (int) (elapsed / 100) % 10; if (hours > 0) { return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds); } else if (withMilliseconds) { diff --git a/src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java b/src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java new file mode 100644 index 000000000..739f6e45c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java @@ -0,0 +1,38 @@ +package eu.siacs.conversations.utils; + +import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy; +import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy; + +public final class TranscoderStrategies { + + public static DefaultVideoStrategy VIDEO(final long bitrate, final int resolution) { + return DefaultVideoStrategy.atMost(resolution) + .bitRate(bitrate) + .frameRate(30) + .keyFrameInterval(3F) + .build(); + } + // see suggested bit rates on https://www.videoproc.com/media-converter/bitrate-setting-for-h264.htm + + public static final DefaultAudioStrategy AUDIO_HQ = DefaultAudioStrategy.builder() + .bitRate(192 * 1000) + .channels(DefaultAudioStrategy.CHANNELS_AS_INPUT) + .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT) + .build(); + + public static final DefaultAudioStrategy AUDIO_MQ = DefaultAudioStrategy.builder() + .bitRate(128 * 1000) + .channels(DefaultAudioStrategy.CHANNELS_AS_INPUT) + .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT) + .build(); + + public static final DefaultAudioStrategy AUDIO_LQ = DefaultAudioStrategy.builder() + .bitRate(96 * 1000) + .channels(1) + .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT) + .build(); + + private TranscoderStrategies() { + throw new IllegalStateException("Do not instantiate me"); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 3a5fa1289..58edbef5b 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -22,6 +22,7 @@ import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.TimeZone; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -39,11 +40,9 @@ import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.ExportBackupService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.util.MyLinkify; +import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.xmpp.Jid; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; - public class UIHelper { private static final int[] UNSAFE_COLORS = { @@ -143,19 +142,22 @@ public class UIHelper { | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE; public static String readableTimeDifference(Context context, long time) { - return readableTimeDifference(context, time, false); + return readableTimeDifference(context, time, false, false); } public static String readableTimeDifferenceFull(Context context, long time) { - return readableTimeDifference(context, time, true); + return readableTimeDifference(context, time, true, false); } - private static String readableTimeDifference(Context context, long time, - boolean fullDate) { + public static String readableTimeDifference(Context context, long time, + boolean fullDate, boolean printTz) { if (time == 0) { return context.getString(R.string.just_now); } Date date = new Date(time); + TimeZone tz = TimeZone.getDefault(); + String tzString = "(" + tz.getDisplayName(tz.inDaylightTime(date), + TimeZone.SHORT, Locale.UK) + ")"; long difference = (System.currentTimeMillis() - time) / 1000; if (difference < 60) { return context.getString(R.string.just_now); @@ -165,18 +167,40 @@ public class UIHelper { return context.getString(R.string.minutes_ago, Math.round(difference / 60.0)); } else if (today(date)) { java.text.DateFormat df = DateFormat.getTimeFormat(context); - return df.format(date); + return printTz ? df.format(date) + " " + tzString : df.format(date); } else { if (fullDate) { - return DateUtils.formatDateTime(context, date.getTime(), + return printTz ? DateUtils.formatDateTime(context, date.getTime(), + FULL_DATE_FLAGS) + " " + tzString : DateUtils.formatDateTime(context, date.getTime(), FULL_DATE_FLAGS); } else { - return DateUtils.formatDateTime(context, date.getTime(), + return printTz ? DateUtils.formatDateTime(context, date.getTime(), + SHORT_DATE_FLAGS) + " " + tzString : DateUtils.formatDateTime(context, date.getTime(), SHORT_DATE_FLAGS); } } } + public static String readableDateTime(Context context, long time, + boolean fullDate, boolean printTz) { + if (time == 0) { + return context.getString(R.string.just_now); + } + Date date = new Date(time); + TimeZone tz = TimeZone.getDefault(); + String tzString = "(" + tz.getDisplayName(tz.inDaylightTime(date), + TimeZone.SHORT, Locale.UK) + ")"; + if (fullDate) { + return printTz ? DateUtils.formatDateTime(context, date.getTime(), + FULL_DATE_FLAGS) + " " + tzString : DateUtils.formatDateTime(context, date.getTime(), + FULL_DATE_FLAGS); + } else { + return printTz ? DateUtils.formatDateTime(context, date.getTime(), + SHORT_DATE_FLAGS) + " " + tzString : DateUtils.formatDateTime(context, date.getTime(), + SHORT_DATE_FLAGS); + } + } + private static boolean today(Date date) { return sameDay(date, new Date(System.currentTimeMillis())); } @@ -317,7 +341,7 @@ public class UIHelper { } } else { final String body = MessageUtils.filterLtrRtl(message.getBody()); - if (message.getBody().equals(DELETED_MESSAGE_BODY) || message.getBody().equals(DELETED_MESSAGE_BODY_OLD)) { + if (message.hasDeletedBody()) { return new Pair<>(context.getString(R.string.message_deleted), false); } else if (body.startsWith(Message.ME_COMMAND)) { return new Pair<>(body.replaceAll("^" + Message.ME_COMMAND, UIHelper.getMessageDisplayName(message)), false); @@ -331,7 +355,7 @@ public class UIHelper { } else { SpannableStringBuilder styledBody = new SpannableStringBuilder(MyLinkify.replaceYoutube(context, body)); if (textColor != 0) { - StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor); + StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor, true); } SpannableStringBuilder builder = new SpannableStringBuilder(); for (CharSequence l : CharSequenceUtils.split(styledBody, '\n')) { @@ -340,7 +364,7 @@ public class UIHelper { continue; } char first = l.charAt(0); - if ((first != '>' || !isPositionFollowedByQuoteableCharacter(l, 0)) && first != '\u00bb') { + if ((!QuoteHelper.isPositionQuoteStart(l, 0))) { CharSequence line = CharSequenceUtils.trim(l); if (line.length() == 0) { continue; @@ -384,14 +408,6 @@ public class UIHelper { return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input; } - public static boolean isPositionFollowedByWhitespace(CharSequence body, int pos){ - return Character.isWhitespace(body.charAt(pos + 1)); - } - - public static boolean isPositionPrecededByWhitespace(CharSequence body, int pos){ - return Character.isWhitespace(body.charAt(pos -1 )); - } - public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos){ // true if not a single linebreak before current position for (int i = pos - 1; i >= 0; i--){ @@ -406,10 +422,7 @@ public class UIHelper { if (isPositionPrecededByBodyStart(body, pos)){ return true; } - if (body.charAt(pos - 1) == '\n'){ - return true; - } - return false; + return body.charAt(pos - 1) == '\n'; } public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) { @@ -454,31 +467,13 @@ public class UIHelper { final char c = body.charAt(i); if (Character.isWhitespace(c)) { return false; - } else if (c == '<' || c == '>') { + } else if (QuoteHelper.isPositionQuoteCharacter(body, pos) || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) { return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1)); } } return false; } - public static boolean isPositionFollowedByQuote(CharSequence body, int pos) { - if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) { - return false; - } - boolean previousWasWhitespace = false; - for (int i = pos + 1; i < body.length(); i++) { - char c = body.charAt(i); - if (c == '\n' || c == '»') { - return false; - } else if (c == '«' && !previousWasWhitespace) { - return true; - } else { - previousWasWhitespace = Character.isWhitespace(c); - } - } - return false; - } - public static String getDisplayName(MucOptions.User user) { Contact contact = user.getContact(); if (contact != null) { @@ -516,6 +511,9 @@ public class UIHelper { } public static String getFileDescriptionString(final Context context, final Message message) { + if (message.getType() == Message.TYPE_IMAGE) { + return context.getString(R.string.image); + } final String mime = message.getMimeType(); if (mime == null) { return context.getString(R.string.file); @@ -525,9 +523,7 @@ public class UIHelper { return context.getString(R.string.video); } else if (mime.equals("image/gif")) { return context.getString(R.string.gif); - } else if (mime.equals("image/svg+xml")) { - return context.getString(R.string.vector_graphic); - } else if (mime.startsWith("image/") || message.getType() == Message.TYPE_IMAGE) { + } else if (mime.startsWith("image/")) { return context.getString(R.string.image); } else if (mime.contains("pdf")) { return context.getString(R.string.pdf_document); @@ -598,8 +594,6 @@ public class UIHelper { } else { return context.getString(R.string.send_message_to_x, conversation.getName()); } - case Message.ENCRYPTION_OTR: - return context.getString(R.string.send_otr_message); case Message.ENCRYPTION_AXOLOTL: AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) { diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index 08d979e84..d1013d6f8 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -4,18 +4,22 @@ import android.net.Uri; import androidx.annotation.NonNull; +import com.google.common.base.CharMatcher; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.Jid; public class XmppUri { @@ -138,10 +142,10 @@ public class XmppUri { return; } this.uri = uri; - String scheme = uri.getScheme(); - String host = uri.getHost(); + final String scheme = uri.getScheme(); + final String host = uri.getHost(); List segments = uri.getPathSegments(); - if ("https".equalsIgnoreCase(scheme) && "monocles.de".equalsIgnoreCase(host)) { + if ("https".equalsIgnoreCase(scheme) && Config.INVITE_DOMAIN.equalsIgnoreCase(host)) { if (segments.size() >= 2 && segments.get(1).contains("@")) { // sample : https://conversations.im/i/foo@bar.com try { @@ -172,7 +176,7 @@ public class XmppUri { } } this.fingerprints = parseFingerprints(parameters); - } else if ("imto".equalsIgnoreCase(scheme)) { + } else if ("imto".equalsIgnoreCase(scheme) && Arrays.asList("xmpp", "jabber").contains(uri.getHost())) { // sample: imto://xmpp/foo@bar.com try { jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1].trim(); @@ -194,7 +198,10 @@ public class XmppUri { } public boolean isAction(final String action) { - return parameters.containsKey(action); + return Collections2.transform( + parameters.keySet(), + s -> CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('A', 'Z')).retainFrom(s) + ).contains(action); } public Jid getJid() { diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 42938c4c0..13eab1ea2 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xml; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.Hashtable; import java.util.List; @@ -120,13 +122,13 @@ public class Element { public Element setAttribute(String name, String value) { if (name != null && value != null) { this.attributes.put(name, value); - } - return this; - } + } + return this; + } - public Element setAttribute(String name, Jid value) { - if (name != null && value != null) { - this.attributes.put(name, value.toEscapedString()); + public Element setAttribute(String name, Jid value) { + if (name != null && value != null) { + this.attributes.put(name, value.toEscapedString()); } return this; } @@ -165,8 +167,9 @@ public class Element { return this.attributes; } + @NotNull public String toString() { - StringBuilder elementOutput = new StringBuilder(); + final StringBuilder elementOutput = new StringBuilder(); if ((content == null) && (children.size() == 0)) { Tag emptyTag = Tag.empty(name); emptyTag.setAtttributes(this.attributes); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index dd3123ecf..41f9093f0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -56,7 +56,6 @@ import javax.net.ssl.X509TrustManager; import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.DomainHostnameVerifier; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.Anonymous; @@ -122,22 +121,22 @@ public class XmppConnection implements Runnable { Element error = packet.findChild("error"); Account.State state = Account.State.REGISTRATION_FAILED; deleteAccount(account); - if (error != null) { - if (error.hasChild("text")) { - errorMessage = error.findChildContent("text"); - Log.d(Config.LOGTAG, "Error creating account : " + error.findChildContent("text")); - } - if (error.hasChild("conflict")) { - state = Account.State.REGISTRATION_CONFLICT; - } else if (error.hasChild("resource-constraint") - && "wait".equals(error.getAttribute("type"))) { - state = Account.State.REGISTRATION_PLEASE_WAIT; - } else if (error.hasChild("not-acceptable") - && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) { - state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; - } + if (error != null) { + if (error.hasChild("text")) { + errorMessage = error.findChildContent("text"); + Log.d(Config.LOGTAG, "Error creating account : " + error.findChildContent("text")); } - Log.d(Config.LOGTAG, "Delete account because of error " + error); + if (error.hasChild("conflict")) { + state = Account.State.REGISTRATION_CONFLICT; + } else if (error.hasChild("resource-constraint") + && "wait".equals(error.getAttribute("type"))) { + state = Account.State.REGISTRATION_PLEASE_WAIT; + } else if (error.hasChild("not-acceptable") + && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) { + state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + } + } + Log.d(Config.LOGTAG, "Delete account because of error " + error); throw new StateChangingError(state); } }; @@ -197,6 +196,10 @@ public class XmppConnection implements Runnable { this.mXmppConnectionService = service; } + public XmppConnectionService getXmppConnectionService() { + return mXmppConnectionService; + } + private void fixResource(Context context, Account account) { String resource = account.getResource(); if (resource != null && !resource.startsWith(context.getString(R.string.app_name) + '[' + BuildConfig.VERSION_NAME + ']')) { @@ -854,7 +857,12 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.TLS_ERROR); } final InetAddress address = socket.getInetAddress(); - final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostAddress(), socket.getPort(), true); + final SSLSocket sslSocket; + try { + sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostAddress(), socket.getPort(), true); + } catch (Exception e) { + throw new StateChangingException(Account.State.TLS_ERROR); + } SSLSocketHelper.setSecurity(sslSocket); SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer())); SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java b/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java index 64acde648..8d3d8ead9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java @@ -1,16 +1,9 @@ package eu.siacs.conversations.xmpp.jid; -import net.java.otr4j.session.SessionID; import eu.siacs.conversations.xmpp.Jid; public final class OtrJidHelper { - public static Jid fromSessionID(final SessionID id) throws IllegalArgumentException { - if (id.getUserID().isEmpty()) { - return Jid.of(id.getAccountID()); - } else { - return Jid.of(id.getAccountID() + "/" + id.getUserID()); - } - } + } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index d4ef544b5..d4fc81608 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -206,7 +206,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final Element error = response.addChild("error"); error.setAttribute("type", conditionType); error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1"); + error.addChild(jingleCondition, Namespace.JINGLE_ERRORS); account.getXmppConnection().sendIqPacket(response, null); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 91523c62b..6ac2e98ab 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,10 +1,13 @@ package eu.siacs.conversations.xmpp.jingle; -import android.os.SystemClock; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; @@ -12,20 +15,25 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; 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; +import com.google.common.util.concurrent.MoreExecutors; import org.webrtc.EglBase; import org.webrtc.IceCandidate; import org.webrtc.PeerConnection; import org.webrtc.VideoTrack; -import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -133,7 +141,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final ArrayDeque>> pendingIceCandidates = new ArrayDeque<>(); + private final Queue> pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; private State state = State.NULL; @@ -141,8 +149,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; - private long rtpConnectionStarted = 0; //time of 'connected' - private long rtpConnectionEnded = 0; + private IceUdpTransportInfo.Setup peerDtlsSetup; + private final Stopwatch sessionDuration = Stopwatch.createUnstarted(); + private final Queue stateHistory = new LinkedList<>(); private ScheduledFuture ringingTimeoutFuture; JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { @@ -184,7 +193,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override synchronized void deliverPacket(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); switch (jinglePacket.getAction()) { case SESSION_INITIATE: receiveSessionInitiate(jinglePacket); @@ -243,25 +251,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void receiveTransportInfo(final JinglePacket jinglePacket) { - if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { - respondOk(jinglePacket); + //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received + if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); - } catch (IllegalArgumentException | NullPointerException e) { + } catch (final IllegalArgumentException | NullPointerException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); + respondOk(jinglePacket); return; } - final Set> candidates = contentMap.contents.entrySet(); - if (this.state == State.SESSION_ACCEPTED) { - try { - processCandidates(candidates); - } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); - } - } else { - pendingIceCandidates.push(candidates); - } + receiveTransportInfo(jinglePacket, contentMap); } else { if (isTerminated()) { respondOk(jinglePacket); @@ -273,55 +273,184 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Set> candidates = contentMap.contents.entrySet(); + if (this.state == State.SESSION_ACCEPTED) { + //zero candidates + modified credentials are an ICE restart offer + if (checkForIceRestart(jinglePacket, contentMap)) { + return; + } + respondOk(jinglePacket); + try { + processCandidates(candidates); + } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); + } + } else { + respondOk(jinglePacket); + pendingIceCandidates.addAll(candidates); + } + } + + private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { + final RtpContentMap existing = getRemoteContentMap(); + final IceUdpTransportInfo.Credentials existingCredentials; + final IceUdpTransportInfo.Credentials newCredentials; + try { + existingCredentials = existing.getCredentials(); + newCredentials = rtpContentMap.getCredentials(); + } catch (final IllegalStateException e) { + Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e); + return false; + } + if (existingCredentials.equals(newCredentials)) { + return false; + } + //TODO an alternative approach is to check if we already got an iq result to our ICE-restart + // and if that's the case we are seeing an answer. + // This might be more spec compliant but also more error prone potentially + final boolean isOffer = rtpContentMap.emptyCandidates(); + final RtpContentMap restartContentMap; + try { + if (isOffer) { + Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); + restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); + } else { + final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); + Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials + " peer_setup=" + setup); + // DTLS setup attribute needs to be rewritten to reflect current peer state + // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM + restartContentMap = existing.modifiedCredentials(newCredentials, setup); + } + if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) { + return isOffer; + } else { + Log.d(Config.LOGTAG, "ignoring ICE restart. sending tie-break"); + respondWithTieBreak(jinglePacket); + return true; + } + } catch (final Exception exception) { + respondOk(jinglePacket); + final Throwable rootCause = Throwables.getRootCause(exception); + if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { + //If this happens a termination is already in progress + Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart"); + return true; + } + Log.d(Config.LOGTAG, "failure to apply ICE restart", rootCause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); + return true; + } + } + + private IceUdpTransportInfo.Setup getPeerDtlsSetup() { + final IceUdpTransportInfo.Setup peerSetup = this.peerDtlsSetup; + if (peerSetup == null || peerSetup == IceUdpTransportInfo.Setup.ACTPASS) { + throw new IllegalStateException("Invalid peer setup"); + } + return peerSetup; + } + + private void storePeerDtlsSetup(final IceUdpTransportInfo.Setup setup) { + if (setup == null || setup == IceUdpTransportInfo.Setup.ACTPASS) { + throw new IllegalArgumentException("Trying to store invalid peer dtls setup"); + } + this.peerDtlsSetup = setup; + } + + private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { + final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); + final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; + org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); + if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) { + if (isInitiator()) { + //We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map + return false; + } + } + webRTCWrapper.setRemoteDescription(sdp).get(); + setRemoteContentMap(restartContentMap); + if (isOffer) { + webRTCWrapper.setIsReadyToReceiveIceCandidates(false); + final SessionDescription localSessionDescription = setLocalSessionDescription(); + setLocalContentMap(RtpContentMap.of(localSessionDescription)); + //We need to respond OK before sending any candidates + respondOk(jinglePacket); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } else { + storePeerDtlsSetup(restartContentMap.getDtlsSetup()); + } + return true; + } + private void processCandidates(final Set> contents) { - final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; + for (final Map.Entry content : contents) { + processCandidate(content); + } + } + + private void processCandidate(final Map.Entry content) { + final RtpContentMap rtpContentMap = getRemoteContentMap(); + final List indices = toIdentificationTags(rtpContentMap); + final String sdpMid = content.getKey(); //aka content name + final IceUdpTransportInfo transport = content.getValue().transport; + final IceUdpTransportInfo.Credentials credentials = transport.getCredentials(); + + //TODO check that credentials remained the same + + for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) { + final String sdp; + try { + sdp = candidate.toSdpAttribute(credentials.ufrag); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + continue; + } + final int mLineIndex = indices.indexOf(sdpMid); + if (mLineIndex < 0) { + Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); + } + final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); + Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); + this.webRTCWrapper.addIceCandidate(iceCandidate); + } + } + + private RtpContentMap getRemoteContentMap() { + return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; + } + + private List toIdentificationTags(final RtpContentMap rtpContentMap) { final Group originalGroup = rtpContentMap.group; final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); if (identificationTags.size() == 0) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } - processCandidates(identificationTags, contents); + return identificationTags; } - private void processCandidates(final List indices, final Set> contents) { - for (final Map.Entry content : contents) { - final String ufrag = content.getValue().transport.getAttribute("ufrag"); - for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { - final String sdp; - try { - sdp = candidate.toSdpAttribute(ufrag); - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); - continue; - } - final String sdpMid = content.getKey(); - final int mLineIndex = indices.indexOf(sdpMid); - if (mLineIndex < 0) { - Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); - } - final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); - Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); - this.webRTCWrapper.addIceCandidate(iceCandidate); - } + private ListenableFuture receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { + final RtpContentMap receivedContentMap; + try { + receivedContentMap = RtpContentMap.of(jinglePacket); + } catch (final Exception e) { + return Futures.immediateFailedFuture(e); } - } - - private RtpContentMap receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { - final RtpContentMap receivedContentMap = RtpContentMap.of(jinglePacket); if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) { - final AxolotlService.OmemoVerifiedPayload omemoVerifiedPayload; - try { - omemoVerifiedPayload = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); - } catch (final CryptoFailedException e) { - throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e); - } - this.omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + this.omemoVerification); - return omemoVerifiedPayload.getPayload(); - } else if (expectVerification) { - throw new SecurityException("DTLS fingerprint was unexpectedly not verifiable"); + final ListenableFuture> future = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); + return Futures.transform(future, omemoVerifiedPayload -> { + //TODO test if an exception here triggers a correct abort + omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + omemoVerification); + return omemoVerifiedPayload.getPayload(); + }, MoreExecutors.directExecutor()); + } else if (Config.REQUIRE_RTP_VERIFICATION || expectVerification) { + return Futures.immediateFailedFuture( + new SecurityException("DTLS fingerprint was unexpectedly not verifiable") + ); } else { - return receivedContentMap; + return Futures.immediateFuture(receivedContentMap); } } @@ -340,11 +469,25 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } return; } - final RtpContentMap contentMap; + final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionInitiate(jinglePacket, rtpContentMap); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, MoreExecutors.directExecutor()); + } + + private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { - contentMap = receiveRtpContentMap(jinglePacket, false); contentMap.requireContentDescriptions(); - contentMap.requireDTLSFingerprint(); + contentMap.requireDTLSFingerprint(true); } catch (final RuntimeException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); @@ -372,11 +515,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { respondOk(jinglePacket); - - final Set> candidates = contentMap.contents.entrySet(); - if (candidates.size() > 0) { - pendingIceCandidates.push(candidates); - } + pendingIceCandidates.addAll(contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); sendSessionAccept(); @@ -396,9 +535,25 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web terminateWithOutOfOrder(jinglePacket); return; } - final RtpContentMap contentMap; + final ListenableFuture future = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionAccept(jinglePacket, rtpContentMap); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", throwable); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, MoreExecutors.directExecutor()); + } + + private void receiveSessionAccept(final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { - contentMap = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { @@ -429,6 +584,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveSessionAccept(final RtpContentMap contentMap) { this.responderRtpContentMap = contentMap; + this.storePeerDtlsSetup(contentMap.getDtlsSetup()); final SessionDescription sessionDescription; try { sessionDescription = SessionDescription.of(contentMap); @@ -447,11 +603,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } catch (final Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); return; } - final List identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags(); - processCandidates(identificationTags, contentMap.contents.entrySet()); + processCandidates(contentMap.contents.entrySet()); } private void sendSessionAccept() { @@ -495,45 +650,77 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { this.webRTCWrapper.setRemoteDescription(sdp).get(); addIceCandidatesFromBlackLog(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionAccept(respondingRtpContentMap); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + prepareSessionAccept(webRTCSessionDescription); } catch (final Exception e) { - Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(e)); - webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + failureToAcceptSession(e); } } + private void failureToAcceptSession(final Throwable throwable) { + if (isTerminated()) { + return; + } + final Throwable rootCause = Throwables.getRootCause(throwable); + Log.d(Config.LOGTAG, "unable to send session accept", rootCause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); + } + private void addIceCandidatesFromBlackLog() { - while (!this.pendingIceCandidates.isEmpty()) { - processCandidates(this.pendingIceCandidates.poll()); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log"); + Map.Entry foo; + while ((foo = this.pendingIceCandidates.poll()) != null) { + processCandidate(foo); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log"); } } + private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSessionDescription) { + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); + this.responderRtpContentMap = respondingRtpContentMap; + storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); + Futures.addCallback(outgoingContentMapFuture, + new FutureCallback() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendSessionAccept(outgoingContentMap); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + failureToAcceptSession(throwable); + } + }, + MoreExecutors.directExecutor() + ); + } + private void sendSessionAccept(final RtpContentMap rtpContentMap) { - this.responderRtpContentMap = rtpContentMap; - this.transitionOrThrow(State.SESSION_ACCEPTED); - final RtpContentMap outgoingContentMap; - if (this.omemoVerification.hasDeviceId()) { - final AxolotlService.OmemoVerifiedPayload verifiedPayload; - try { - verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - outgoingContentMap = verifiedPayload.getPayload(); - this.omemoVerification.setOrEnsureEqual(verifiedPayload); - } catch (final Exception e) { - throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e); - } - } else { - outgoingContentMap = rtpContentMap; + if (isTerminated()) { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); + return; } - final JinglePacket sessionAccept = outgoingContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + transitionOrThrow(State.SESSION_ACCEPTED); + final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); } + private ListenableFuture prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { + if (this.omemoVerification.hasDeviceId()) { + ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() + .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); + return Futures.transform(verifiedPayloadFuture, verifiedPayload -> { + omemoVerification.setOrEnsureEqual(verifiedPayload); + return verifiedPayload.getPayload(); + }, MoreExecutors.directExecutor()); + } else { + return Futures.immediateFuture(rtpContentMap); + } + } + synchronized void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); switch (message.getName()) { @@ -764,52 +951,90 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } catch (final WebRTCWrapper.InitializationException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC"); webRTCWrapper.close(); - sendJingleMessage("retract", id.with.asBareJid()); - transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); - this.finish(); + sendRetract(Reason.ofThrowable(e)); return; } try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionInitiate(rtpContentMap, targetState); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + prepareSessionInitiate(webRTCSessionDescription, targetState); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(e)); - webRTCWrapper.close(); - if (isInState(targetState)) { - sendSessionTerminate(Reason.FAILED_APPLICATION); - } else { - sendJingleMessage("retract", id.with.asBareJid()); - transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); - this.finish(); - } + //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions + failureToInitiateSession(e, targetState); } } - private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { + private void failureToInitiateSession(final Throwable throwable, final State targetState) { + if (isTerminated()) { + return; + } + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(throwable)); + webRTCWrapper.close(); + final Reason reason = Reason.ofThrowable(throwable); + if (isInState(targetState)) { + sendSessionTerminate(reason); + } else { + sendRetract(reason); + } + } + + private void sendRetract(final Reason reason) { + //TODO embed reason into retract + sendJingleMessage("retract", id.with.asBareJid()); + transitionOrThrow(reasonToState(reason)); + this.finish(); + } + + private void prepareSessionInitiate(final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; + this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); + Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendSessionInitiate(outgoingContentMap, targetState); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + failureToInitiateSession(throwable, targetState); + } + }, MoreExecutors.directExecutor()); + } + + private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { + if (isTerminated()) { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); + return; + } this.transitionOrThrow(targetState); - //TODO do on background thread? - final RtpContentMap outgoingContentMap = encryptSessionInitiate(rtpContentMap); - final JinglePacket sessionInitiate = outgoingContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); } - private RtpContentMap encryptSessionInitiate(final RtpContentMap rtpContentMap) { + private ListenableFuture encryptSessionInitiate(final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - final AxolotlService.OmemoVerifiedPayload verifiedPayload; - try { - verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - } catch (final CryptoFailedException e) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); - return rtpContentMap; + final ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() + .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); + final ListenableFuture future = Futures.transform(verifiedPayloadFuture, verifiedPayload -> { + omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint()); + return verifiedPayload.getPayload(); + }, MoreExecutors.directExecutor()); + if (Config.REQUIRE_RTP_VERIFICATION) { + return future; } - this.omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint()); - return verifiedPayload.getPayload(); + return Futures.catching( + future, + CryptoFailedException.class, + e -> { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); + return rtpContentMap; + }, + MoreExecutors.directExecutor() + ); } else { - return rtpContentMap; + return Futures.immediateFuture(rtpContentMap); } } @@ -851,36 +1076,48 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private synchronized void handleIqResponse(final Account account, final IqPacket response) { if (response.getType() == IqPacket.TYPE.ERROR) { - final String errorCondition = response.getErrorCondition(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); - if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - final State target; - if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout" - ).contains(errorCondition)) { - target = State.TERMINATED_CONNECTIVITY_ERROR; - } else { - target = State.TERMINATED_APPLICATION_FAILURE; - } - transitionOrThrow(target); - this.finish(); - } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); - if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); - this.finish(); + handleIqErrorResponse(response); + return; } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + } + + private void handleIqErrorResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + final String errorCondition = response.getErrorCondition(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); + if (isTerminated()) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; + } + this.webRTCWrapper.close(); + final State target; + if (Arrays.asList( + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout" + ).contains(errorCondition)) { + target = State.TERMINATED_CONNECTIVITY_ERROR; + } else { + target = State.TERMINATED_APPLICATION_FAILURE; + } + transitionOrThrow(target); + this.finish(); + } + + private void handleIqTimeoutResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); + if (isTerminated()) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; + } + this.webRTCWrapper.close(); + transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); + this.finish(); } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { @@ -891,8 +1128,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.finish(); } + private void respondWithTieBreak(final JinglePacket jinglePacket) { + respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel"); + } + private void respondWithOutOfOrder(final JinglePacket jinglePacket) { - jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait"); + respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); + } + + void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) { + jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType); } private void respondOk(final JinglePacket jinglePacket) { @@ -929,23 +1174,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.CONNECTING; } case SESSION_ACCEPTED: - final PeerConnection.PeerConnectionState state; - try { - state = webRTCWrapper.getState(); - } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - //We usually close the WebRTCWrapper *before* transitioning so we might still - //be in SESSION_ACCEPTED even though the peerConnection has been torn down - return RtpEndUserState.ENDING_CALL; - } - if (state == PeerConnection.PeerConnectionState.CONNECTED) { - return RtpEndUserState.CONNECTED; - } else if (state == PeerConnection.PeerConnectionState.NEW || state == PeerConnection.PeerConnectionState.CONNECTING) { - return RtpEndUserState.CONNECTING; - } else if (state == PeerConnection.PeerConnectionState.CLOSED) { - return RtpEndUserState.ENDING_CALL; - } else { - return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; - } + return getPeerConnectionStateAsEndUserState(); case REJECTED: case REJECTED_RACED: case TERMINATED_DECLINED_OR_BUSY: @@ -966,7 +1195,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.RETRACTED; } case TERMINATED_CONNECTIVITY_ERROR: - return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; + return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; case TERMINATED_APPLICATION_FAILURE: return RtpEndUserState.APPLICATION_ERROR; case TERMINATED_SECURITY_ERROR: @@ -975,6 +1204,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); } + + private RtpEndUserState getPeerConnectionStateAsEndUserState() { + final PeerConnection.PeerConnectionState state; + try { + state = webRTCWrapper.getState(); + } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { + //We usually close the WebRTCWrapper *before* transitioning so we might still + //be in SESSION_ACCEPTED even though the peerConnection has been torn down + return RtpEndUserState.ENDING_CALL; + } + switch (state) { + case CONNECTED: + return RtpEndUserState.CONNECTED; + case NEW: + case CONNECTING: + return RtpEndUserState.CONNECTING; + case CLOSED: + return RtpEndUserState.ENDING_CALL; + default: + return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING; + } + } + public Set getMedia() { final State current = getState(); if (current == State.NULL) { @@ -1009,7 +1261,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return false; } final FingerprintStatus status = id.account.getAxolotlService().getFingerprintTrust(fingerprint); - return status != null && status.getTrust() == FingerprintStatus.Trust.VERIFIED; + return status != null && status.isVerified(); } public synchronized void acceptCall() { @@ -1217,7 +1469,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onIceCandidate(final IceCandidate iceCandidate) { - final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); + final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final String ufrag = rtpContentMap.getCredentials().ufrag; + final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); + if (candidate == null) { + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString()); + return; + } Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); sendTransportInfo(iceCandidate.sdpMid, candidate); } @@ -1225,26 +1483,97 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); - if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { - this.rtpConnectionStarted = SystemClock.elapsedRealtime(); + this.stateHistory.add(newState); + if (newState == PeerConnection.PeerConnectionState.CONNECTED) { + this.sessionDuration.start(); + updateOngoingCallNotification(); + } else if (this.sessionDuration.isRunning()) { + this.sessionDuration.stop(); + updateOngoingCallNotification(); } - if (newState == PeerConnection.PeerConnectionState.CLOSED && this.rtpConnectionEnded == 0) { - this.rtpConnectionEnded = SystemClock.elapsedRealtime(); + + final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); + + if (newState == PeerConnection.PeerConnectionState.FAILED) { + if (neverConnected) { + if (isTerminated()) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + return; + } + webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); + return; + } else { + webRTCWrapper.restartIce(); + } } - //TODO 'DISCONNECTED' might be an opportunity to renew the offer and send a transport-replace - //TODO exact syntax is yet to be determined but transport-replace sounds like the most reasonable - //as there is no content-replace - if (Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { - if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + updateEndUserState(); + } + + @Override + public void onRenegotiationNeeded() { + this.webRTCWrapper.execute(this::initiateIceRestart); + } + + private void initiateIceRestart() { + //TODO discover new TURN/STUN credentials + this.stateHistory.clear(); + this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); + final SessionDescription sessionDescription; + try { + sessionDescription = setLocalSessionDescription(); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d(Config.LOGTAG, "failed to renegotiate", cause); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap transportInfo = rtpContentMap.transportInfo(); + final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); + jinglePacket.setTo(id.with); + xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "received success to our ice restart"); + setLocalContentMap(rtpContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); return; } - new Thread(this::closeWebRTCSessionAfterFailedConnection).start(); + if (response.getType() == IqPacket.TYPE.ERROR) { + final Element error = response.findChild("error"); + if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); + } + + private void setLocalContentMap(final RtpContentMap rtpContentMap) { + if (isInitiator()) { + this.initiatorRtpContentMap = rtpContentMap; } else { - updateEndUserState(); + this.responderRtpContentMap = rtpContentMap; } } + private void setRemoteContentMap(final RtpContentMap rtpContentMap) { + if (isInitiator()) { + this.responderRtpContentMap = rtpContentMap; + } else { + this.initiatorRtpContentMap = rtpContentMap; + } + } + + private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { + final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get(); + return SessionDescription.parse(sessionDescription.description); + } + private void closeWebRTCSessionAfterFailedConnection() { this.webRTCWrapper.close(); synchronized (this) { @@ -1256,12 +1585,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - public long getRtpConnectionStarted() { - return this.rtpConnectionStarted; + public boolean zeroDuration() { + return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0; } - public long getRtpConnectionEnded() { - return this.rtpConnectionEnded; + public long getCallDuration() { + return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); } public AppRTCAudioManager getAudioManager() { @@ -1308,8 +1637,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void updateOngoingCallNotification() { - if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) { - xmppConnectionService.setOngoingCall(id, getMedia()); + final State state = this.state; + if (STATES_SHOWING_ONGOING_CALL.contains(state)) { + final boolean reconnecting; + if (state == State.SESSION_ACCEPTED) { + reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; + } else { + reconnecting = false; + } + xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting); } else { xmppConnectionService.removeOngoingCall(); } @@ -1388,8 +1724,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void writeLogMessage(final State state) { - final long started = this.rtpConnectionStarted; - long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started; + final long duration = getCallDuration(); if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { writeLogMessageSuccess(duration); } else { @@ -1435,7 +1770,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return webRTCWrapper.getRemoteVideoTrack(); } - public EglBase.Context getEglBaseContext() { return webRTCWrapper.getEglBaseContext(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 3df379b96..8ecdb7a4f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.os.PowerManager; import android.util.Log; +import com.google.common.io.ByteStreams; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -56,22 +58,12 @@ public class JingleSocks5Transport extends JingleTransport { } else { destBuilder.append(this.connection.getTransportId()); } - if (candidate.getType() == JingleCandidate.TYPE_PROXY) { - if (candidate.isOurs()) { - destBuilder.append(this.account.getJid()); - destBuilder.append(this.connection.getId().with); - } else { - destBuilder.append(this.connection.getId().with); - destBuilder.append(this.account.getJid()); - } + if (candidate.isOurs()) { + destBuilder.append(this.account.getJid()); + destBuilder.append(this.connection.getId().with); } else { - if (connection.isInitiator()) { - destBuilder.append(this.account.getJid()); - destBuilder.append(this.connection.getId().with); - } else { - destBuilder.append(this.connection.getId().with); - destBuilder.append(this.account.getJid()); - } + destBuilder.append(this.connection.getId().with); + destBuilder.append(this.account.getJid()); } messageDigest.reset(); this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes())); @@ -112,26 +104,26 @@ public class JingleSocks5Transport extends JingleTransport { final byte[] authBegin = new byte[2]; final InputStream inputStream = socket.getInputStream(); final OutputStream outputStream = socket.getOutputStream(); - inputStream.read(authBegin); + ByteStreams.readFully(inputStream, authBegin); if (authBegin[0] != 0x5) { socket.close(); } final short methodCount = authBegin[1]; final byte[] methods = new byte[methodCount]; - inputStream.read(methods); + ByteStreams.readFully(inputStream, methods); if (SocksSocketFactory.contains((byte) 0x00, methods)) { outputStream.write(new byte[]{0x05, 0x00}); } else { outputStream.write(new byte[]{0x05, (byte) 0xff}); } - byte[] connectCommand = new byte[4]; - inputStream.read(connectCommand); + final byte[] connectCommand = new byte[4]; + ByteStreams.readFully(inputStream, connectCommand); if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) { int destinationCount = inputStream.read(); final byte[] destination = new byte[destinationCount]; - inputStream.read(destination); + ByteStreams.readFully(inputStream, destination); final byte[] port = new byte[2]; - inputStream.read(port); + ByteStreams.readFully(inputStream, port); final String receivedDestination = new String(destination); final ByteBuffer response = ByteBuffer.allocate(7 + destination.length); final byte[] responseHeader; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java index 0be0f2cf7..48ff0672c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java @@ -55,7 +55,7 @@ public class OmemoVerification { throw new IllegalStateException("No session fingerprint has been previously provided"); } if (!sessionFingerprint.equals(this.sessionFingerprint)) { - throw new IllegalStateException("Session Fingerprints did not match"); + throw new SecurityException("Session Fingerprints did not match"); } if (this.deviceId == null) { throw new IllegalStateException("No Device Id has been previously provided"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 3e02cc29b..4c81d4884 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,13 +1,12 @@ package eu.siacs.conversations.xmpp.jingle; -import android.util.Log; - import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -17,9 +16,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -97,6 +96,10 @@ public class RtpContentMap { } void requireDTLSFingerprint() { + requireDTLSFingerprint(false); + } + + void requireDTLSFingerprint(final boolean requireActPass) { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } @@ -106,9 +109,13 @@ public class RtpContentMap { if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); } - if (Strings.isNullOrEmpty(fingerprint.getSetup())) { + final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); + if (setup == null) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); } + if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) { + throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); + } } } @@ -137,7 +144,56 @@ public class RtpContentMap { final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); newTransportInfo.addChild(candidate); return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + } + RtpContentMap transportInfo() { + return new RtpContentMap( + null, + Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())) + ); + } + + public IceUdpTransportInfo.Credentials getCredentials() { + final Set allCredentials = ImmutableSet.copyOf(Collections2.transform( + contents.values(), + dt -> dt.transport.getCredentials() + )); + final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); + if (allCredentials.size() == 1 && credentials != null) { + return credentials; + } + throw new IllegalStateException("Content map does not have distinct credentials"); + } + + public IceUdpTransportInfo.Setup getDtlsSetup() { + final Set setups = ImmutableSet.copyOf(Collections2.transform( + contents.values(), + dt -> dt.transport.getFingerprint().getSetup() + )); + final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null); + if (setups.size() == 1 && setup != null) { + return setup; + } + throw new IllegalStateException("Content map doesn't have distinct DTLS setup"); + } + + public boolean emptyCandidates() { + int count = 0; + for (DescriptionTransport descriptionTransport : contents.values()) { + count += descriptionTransport.transport.getCandidates().size(); + } + return count == 0; + } + + public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { + final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + for (final Map.Entry content : contents.entrySet()) { + final RtpDescription rtpDescription = content.getValue().description; + IceUdpTransportInfo transportInfo = content.getValue().transport; + final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); + contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + } + return new RtpContentMap(this.group, contentMapBuilder.build()); } public static class DescriptionTransport { @@ -159,7 +215,6 @@ public class RtpContentMap { } else if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { - Log.d(Config.LOGTAG, "description was " + description); throw new UnsupportedApplicationException("Content does not contain rtp description"); } if (transportInfo instanceof IceUdpTransportInfo) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 974ad4511..9a431bc01 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -4,6 +4,7 @@ public enum RtpEndUserState { INCOMING_CALL, //received a 'propose' message CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet CONNECTED, //session-accepted and webrtc peer connection is connected + RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet RINGING, //'propose' has been sent out and it has been 184 acked ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received @@ -15,4 +16,4 @@ public enum RtpEndUserState { 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 - } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index b518a571a..c653a3dcb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -157,7 +157,10 @@ public class SessionDescription { final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); - mediaAttributes.put("setup", fingerprint.getSetup()); + final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); + if (setup != null) { + mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); + } } final ImmutableList.Builder formatBuilder = new ImmutableList.Builder<>(); for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index 4fb9dee16..e368d3b09 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -51,7 +51,7 @@ class ToneManager { return ToneState.ENDING_CALL; } } - if (state == RtpEndUserState.CONNECTED) { + if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) { if (media.contains(Media.VIDEO)) { return ToneState.NULL; } else { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index b1c212007..6722f9f2c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -17,7 +17,6 @@ import com.google.common.util.concurrent.SettableFuture; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; -import org.webrtc.Camera1Enumerator; import org.webrtc.Camera2Enumerator; import org.webrtc.CameraEnumerationAndroid; import org.webrtc.CameraEnumerator; @@ -45,8 +44,13 @@ import org.webrtc.voiceengine.WebRtcAudioEffects; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -59,6 +63,8 @@ public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + //we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296 private static final Set HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder() .add("Pixel") @@ -79,6 +85,8 @@ public class WebRTCWrapper { private static final int CAPTURING_MAX_FRAME_RATE = 30; private final EventCallback eventCallback; + private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); + private final Queue iceCandidates = new LinkedList<>(); private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { @@ -98,13 +106,13 @@ public class WebRTCWrapper { } @Override - public void onConnectionChange(PeerConnection.PeerConnectionState newState) { + public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { eventCallback.onConnectionChange(newState); } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { - + Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")"); } @Override @@ -125,7 +133,11 @@ public class WebRTCWrapper { @Override public void onIceCandidate(IceCandidate iceCandidate) { - eventCallback.onIceCandidate(iceCandidate); + if (readyToReceivedIceCandidates.get()) { + eventCallback.onIceCandidate(iceCandidate); + } else { + iceCandidates.add(iceCandidate); + } } @Override @@ -136,7 +148,6 @@ public class WebRTCWrapper { @Override public void onAddStream(MediaStream mediaStream) { Log.d(EXTENDED_LOGGING_TAG, "onAddStream(numAudioTracks=" + mediaStream.audioTracks.size() + ",numVideoTracks=" + mediaStream.videoTracks.size() + ")"); - } @Override @@ -151,7 +162,11 @@ public class WebRTCWrapper { @Override public void onRenegotiationNeeded() { - + Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); + final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState(); + if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { + eventCallback.onRenegotiationNeeded(); + } } @Override @@ -161,7 +176,6 @@ public class WebRTCWrapper { if (track instanceof VideoTrack) { remoteVideoTrack = (VideoTrack) track; } - } @Override @@ -253,10 +267,7 @@ public class WebRTCWrapper { .createPeerConnectionFactory(); - final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); - rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp - rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; - rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); @@ -290,6 +301,31 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } + private static PeerConnection.RTCConfiguration buildConfiguration(final List iceServers) { + final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); + rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp + rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; + rtcConfig.enableImplicitRollback = true; + return rtcConfig; + } + + void reconfigurePeerConnection(final List iceServers) { + requirePeerConnection().setConfiguration(buildConfiguration(iceServers)); + } + + void restartIce() { + executorService.execute(() -> requirePeerConnection().restartIce()); + } + + public void setIsReadyToReceiveIceCandidates(final boolean ready) { + readyToReceivedIceCandidates.set(ready); + while (ready && iceCandidates.peek() != null) { + eventCallback.onIceCandidate(iceCandidates.poll()); + } + } + synchronized void close() { final PeerConnection peerConnection = this.peerConnection; final CapturerChoice capturerChoice = this.capturerChoice; @@ -404,70 +440,36 @@ public class WebRTCWrapper { videoTrack.setEnabled(enabled); } - ListenableFuture createOffer() { + synchronized ListenableFuture setLocalDescription() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); - peerConnection.createOffer(new CreateSdpObserver() { - @Override - public void onCreateSuccess(SessionDescription sessionDescription) { - future.set(sessionDescription); - } - - @Override - public void onCreateFailure(String s) { - future.setException(new IllegalStateException("Unable to create offer: " + s)); - } - }, new MediaConstraints()); - return future; - }, MoreExecutors.directExecutor()); - } - - ListenableFuture createAnswer() { - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.createAnswer(new CreateSdpObserver() { - @Override - public void onCreateSuccess(SessionDescription sessionDescription) { - future.set(sessionDescription); - } - - @Override - public void onCreateFailure(String s) { - future.setException(new IllegalStateException("Unable to create answer: " + s)); - } - }, new MediaConstraints()); - return future; - }, MoreExecutors.directExecutor()); - } - - ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { - Log.d(EXTENDED_LOGGING_TAG, "setting local description:"); - for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { - Log.d(EXTENDED_LOGGING_TAG, line); - } - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); peerConnection.setLocalDescription(new SetSdpObserver() { @Override public void onSetSuccess() { - future.set(null); + final SessionDescription description = peerConnection.getLocalDescription(); + Log.d(EXTENDED_LOGGING_TAG, "set local description:"); + logDescription(description); + future.set(description); } @Override - public void onSetFailure(final String s) { - future.setException(new IllegalArgumentException("unable to set local session description: " + s)); - + public void onSetFailure(final String message) { + future.setException(new FailureToSetDescriptionException(message)); } - }, sessionDescription); + }); return future; }, MoreExecutors.directExecutor()); } - ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { - Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); + private static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); } + } + + synchronized ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); + logDescription(sessionDescription); return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.setRemoteDescription(new SetSdpObserver() { @@ -477,9 +479,8 @@ public class WebRTCWrapper { } @Override - public void onSetFailure(String s) { - future.setException(new IllegalArgumentException("unable to set remote session description: " + s)); - + public void onSetFailure(final String message) { + future.setException(new FailureToSetDescriptionException(message)); } }, sessionDescription); return future; @@ -490,26 +491,26 @@ public class WebRTCWrapper { private ListenableFuture getPeerConnectionFuture() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { - return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first")); + return Futures.immediateFailedFuture(new PeerConnectionNotInitialized()); } else { return Futures.immediateFuture(peerConnection); } } + private PeerConnection requirePeerConnection() { + final PeerConnection peerConnection = this.peerConnection; + if (peerConnection == null) { + throw new PeerConnectionNotInitialized(); + } + return peerConnection; + } + void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } - private CameraEnumerator getCameraEnumerator() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return new Camera2Enumerator(requireContext()); - } else { - return new Camera1Enumerator(); - } - } - private Optional getVideoCapturer() { - final CameraEnumerator enumerator = getCameraEnumerator(); + final CameraEnumerator enumerator = new Camera2Enumerator(requireContext()); final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); for (final String deviceName : deviceNames) { if (isFrontFacing(enumerator, deviceName)) { @@ -528,10 +529,15 @@ public class WebRTCWrapper { } } - public PeerConnection.PeerConnectionState getState() { + PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } + public PeerConnection.SignalingState getSignalingState() { + return requirePeerConnection().signalingState(); + } + + EglBase.Context getEglBaseContext() { return this.eglBase.getEglBaseContext(); } @@ -544,14 +550,6 @@ public class WebRTCWrapper { return Optional.fromNullable(this.remoteVideoTrack); } - private PeerConnection requirePeerConnection() { - final PeerConnection peerConnection = this.peerConnection; - if (peerConnection == null) { - throw new PeerConnectionNotInitialized(); - } - return peerConnection; - } - private Context requireContext() { final Context context = this.context; if (context == null) { @@ -564,12 +562,18 @@ public class WebRTCWrapper { return appRTCAudioManager; } + void execute(final Runnable command) { + executorService.execute(command); + } + public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); void onConnectionChange(PeerConnection.PeerConnectionState newState); void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); + + void onRenegotiationNeeded(); } private static abstract class SetSdpObserver implements SdpObserver { @@ -620,6 +624,12 @@ public class WebRTCWrapper { } + private static class FailureToSetDescriptionException extends IllegalArgumentException { + public FailureToSetDescriptionException(String message) { + super(message); + } + } + private static class CapturerChoice { private final CameraVideoCapturer cameraVideoCapturer; private final CameraEnumerationAndroid.CaptureFormat captureFormat; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index a2da9cf1f..ee648acd9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Joiner; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; @@ -8,6 +10,8 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; @@ -57,6 +61,12 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); } + public Credentials getCredentials() { + final String ufrag = this.getAttribute("ufrag"); + final String password = this.getAttribute("pwd"); + return new Credentials(ufrag, password); + } + public List getCandidates() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : getChildren()) { @@ -73,6 +83,53 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return transportInfo; } + public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) { + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + transportInfo.setAttribute("ufrag", credentials.ufrag); + transportInfo.setAttribute("pwd", credentials.password); + for (final Element child : getChildren()) { + if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + final Fingerprint fingerprint = new Fingerprint(); + fingerprint.setAttributes(new Hashtable<>(child.getAttributes())); + fingerprint.setContent(child.getContent()); + fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT)); + transportInfo.addChild(fingerprint); + } + } + return transportInfo; + } + + public static class Credentials { + public final String ufrag; + public final String password; + + public Credentials(String ufrag, String password) { + this.ufrag = ufrag; + this.password = password; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Credentials that = (Credentials) o; + return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hashCode(ufrag, password); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("ufrag", ufrag) + .add("password", password) + .toString(); + } + } + public static class Candidate extends Element { private Candidate() { @@ -88,7 +145,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 - public static Candidate fromSdpAttribute(final String attribute) { + public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) { final String[] pair = attribute.split(":", 2); if (pair.length == 2 && "candidate".equals(pair[0])) { final String[] segments = pair[1].split(" "); @@ -103,6 +160,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo { for (int i = 6; i < segments.length - 1; i = i + 2) { additional.put(segments[i], segments[i + 1]); } + final String ufrag = additional.get("ufrag"); + if (ufrag != null && !ufrag.equals(currentUfrag)) { + return null; + } final Candidate candidate = new Candidate(); candidate.setAttribute("component", component); candidate.setAttribute("foundation", foundation); @@ -282,8 +343,31 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return this.getAttribute("hash"); } - public String getSetup() { - return this.getAttribute("setup"); + public Setup getSetup() { + final String setup = this.getAttribute("setup"); + return setup == null ? null : Setup.of(setup); + } + } + + public enum Setup { + ACTPASS, PASSIVE, ACTIVE; + + public static Setup of(String setup) { + try { + return valueOf(setup.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + + public Setup flip() { + if (this == PASSIVE) { + return ACTIVE; + } + if (this == ACTIVE) { + return PASSIVE; + } + throw new IllegalStateException(this.name()+" can not be flipped"); } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index c419045b0..f734e0685 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -3,7 +3,9 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import androidx.annotation.NonNull; import com.google.common.base.CaseFormat; +import com.google.common.base.Throwables; +import eu.siacs.conversations.crypto.axolotl.CryptoFailedException; import eu.siacs.conversations.xmpp.jingle.RtpContentMap; public enum Reason { @@ -51,4 +53,15 @@ public enum Reason { return FAILED_APPLICATION; } } + + public static Reason ofThrowable(final Throwable throwable) { + final Throwable root = Throwables.getRootCause(throwable); + if (root instanceof RuntimeException) { + return of((RuntimeException) root); + } + if (root instanceof CryptoFailedException) { + return SECURITY_ERROR; + } + return FAILED_APPLICATION; + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java index 7bc1a7c9a..2291a9896 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java @@ -24,10 +24,10 @@ abstract public class AbstractAcknowledgeableStanza extends AbstractStanza { return null; } for (final Element element : error.getChildren()) { - if (!element.getName().equals("text")) { - return element; - } + if (!element.getName().equals("text")) { + return element; } + } return null; } diff --git a/src/main/res/anim/dft.xml b/src/main/res/anim/dft.xml new file mode 100644 index 000000000..c0c0463dd --- /dev/null +++ b/src/main/res/anim/dft.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/main/res/anim/ufb.xml b/src/main/res/anim/ufb.xml new file mode 100644 index 000000000..eb3074c4d --- /dev/null +++ b/src/main/res/anim/ufb.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/main/res/color/button_state_color.xml b/src/main/res/color/button_state_color.xml index db794d038..c16278b3a 100644 --- a/src/main/res/color/button_state_color.xml +++ b/src/main/res/color/button_state_color.xml @@ -1,5 +1,7 @@ - - + + + + \ No newline at end of file diff --git a/src/main/res/color/text_input_stroke_color.xml b/src/main/res/color/text_input_stroke_color.xml new file mode 100644 index 000000000..227339c28 --- /dev/null +++ b/src/main/res/color/text_input_stroke_color.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/res/drawable-hdpi/ic_close_black.png b/src/main/res/drawable-hdpi/ic_close_black.png new file mode 100644 index 0000000000000000000000000000000000000000..566c2c71ef02b6cd355c2f811ca8313faf6d7f77 GIT binary patch literal 236 zcmVDTzk`JU{lDkv-U`jy{1YdH_8{X5>L*u5DvRARQ zeh*xe(q2%Zv(*fu`4m#7NT970!^W4TUnM z_VCeCyx~X0000Vqqv%-5}COAzLaQKEhTEfnV%4iFQr5cIMam~P?Qo`aApW% zP$nfZ;LK3Oz>kzTLsB{qF?bhy;JT>@f%{}41u1ZD83m#2M_)og<{l{_b3F6j8YwXt zU(V)AiGird%#jjW$L zxR(i2gkt(xW=Rv`{Sz}dgJRvC`xT=Jam^dd2=-4}E7uNOSHu`0AoUj#vVHYeErcbL z`~BiV%7#V;r*tA@`=)6^xd}^s?Q;@m?fah&k?6u4KW2{OIRC~A7*RpxFZO-|00000 LNkvXXu0mjf*Jy(7 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_format_bold_white.png b/src/main/res/drawable-hdpi/ic_format_bold_white.png new file mode 100644 index 0000000000000000000000000000000000000000..afe7e19e1dde787cbd785c104bf1e49857ed913b GIT binary patch literal 300 zcmV+{0n`48P)E~zcKeI23C4qphU&-%#^nW^#^eyXl4|k1|%Oc%$NbB&U(5yhE&A8onp;*$brWtIJlUd zZ^L}0^ad+k)A9wr+_`z(u`W@2k2y&=KUDXfxW+yC=1rgANh+thfb`{TgaPW#hrcV$IsB~^0Dv#?&%JXoqxRe s*WHIEIv3ybG3z1`^Hn@2)g0iiJ9s|+v76{zpvM?IUHx3vIVCg!00QW6O8@`> literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_format_italic_white.png b/src/main/res/drawable-hdpi/ic_format_italic_white.png new file mode 100644 index 0000000000000000000000000000000000000000..b585069cceb7d4f9d65f9c7a82e5d9df61a18cf2 GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBj(NH`hE&A8of^n{D1oOXoq>6? zq3n$rjgki#%mfTr8W?P*e#`y2ZHeL(ZvMZMs();bd$%d?XZym;S@y@y#Le&cS0pi` z6Pd6{x%Z-vsqmiN{FnbF<;)iOyf|YiDYtLInaRdRHh-Q}EtrsJs}k01v~XgvQO7X#zsk|?5{8Vebr`ta_b_WIi=-dhk`H0 l^D!GE5%*1eFV`HD&U-#jaer6nETE4VJYD@<);T3K0RS#rY19A! literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_format_monospace_black.png b/src/main/res/drawable-hdpi/ic_format_monospace_black.png new file mode 100644 index 0000000000000000000000000000000000000000..b60a987884b81a54a9ebc26e83714a4b45257a2d GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBW_!9ghE&A8J?F@E*nr37qRgp# zUrhFe<=y5mN?>92lyemSd_W+F_3vvT!HK&ppFg)eegA*b=9xc5x+RnElqq@Vs2R`S zblgS!jpN07CoMyVeydK#C=q40zQr>P1NvTdG?s*<^DJ5PPwzyD+tTC(%By6U&#ZVX zk>GMM>48d@9P`u_+azu*esg<&bza@ia;|8h&c^b(pVfkboizG%DyCHjy$iQ zWX#|D5{mhq;0>c7T~sFqzAkD2E+1h4;Uh)DoNAKQy{mQb0gcVT~ z#ZDqG%kpf*UMaOz6BpQwe9w{|yRcE>W}iM07CE^8~|8Gv7dKc`k2Y zt+0?7e4+yK{ebG>Q-0qqyqf3ph6vqQ$d$RnRM_>z?V8FOm108}W1bfrSzEXFr+sDw zuh6ho*ihqYTXZ065x+QM9aF0S8FOo51+-Xygk&ftbichv*NcojnCk_cA$V4p!5kBk zdBvt+mU)YT$yi|OptJGC-n;fV}c1TkD6 zS3F;b1r4LjX?eaNC@lBcuTcG)DQtC2pi-9hSp&SNW=x=3*xqOdnP@Q{d=V!kBqaPR Y-cP(v(pnOi`2YX_07*qoM6N<$f{_oU&;S4c literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_format_strikethrough_white.png b/src/main/res/drawable-hdpi/ic_format_strikethrough_white.png new file mode 100644 index 0000000000000000000000000000000000000000..3805e338fd9b15ba7a62bb0a38287ce6d82cf61c GIT binary patch literal 379 zcmV->0fhdEP)Nwy!#1Vl0md&J z>=iBuN8enw;`pw^3oz6;MU8^*n3txM*6jZRf{cNs^oIqdAnthceFdF4HVI8Q^JY(R zOo;xzH;Q^Y<|lj+Y!JQZBJnGZ8;rOYv9Apx1+^TPgeJU@K@Vdi4i~8P@|O4VM2Ch6 z1oMr!(}@k;8!;!&mj$u^&pzoYxo0Ot+)VNE#e}$W#e|5PEgbI?6@l+~-tw4ArBbO3 Z;0~e|Rh-JNuSNg>002ovPDHLkV1fz6qFev~ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fca883a15887af5f3117693110ae9caaa75bf14d GIT binary patch literal 698 zcmV;r0!96aP)kl=I8X$(%jx-d&c9 zcjK3Qxs#bQXU?8~Ga&tkc#`<4knz%iA(w@WmySsow34`%_%!iI)gZ>qv4+g8gG06x zKP8^28WPFc$0B^5N^so{L~^LsQ^%N`GOMmgAcDNN3w$%V|LFR4k?cyJeY${xeur1pqqPpNuP%9M0G zJn}?WyZflJ=PP(znc}uQqUp^u>Ll@z2|Mho$_#FdU|V$~?VejZ0#6Y>@`U7tj0scp z4B+Z8@4#ox<@vyJUV659>6a@Y>+OqpIvi@8Q44u~7Nv$VB&NY`sAOB#pm%8hAeh*wk~`w{FxT|0S)j>cxvRB zbChM>W8r%CzYyp^_)_>gIR9>oZwXfm8)kFcbg1ig^f5u-pv*Bd)baWxSm-Xg>Hp0sg6t{0D>&*o8G{pkuQ!X5g!-9Gr{9U8i?p8pL zYpj{ZHp$$5;WKmo%gFH|WrhAj^W}c`vrY+UcGzd!@U+l*Y>T^2=11xjc9Wp<8eMu6 zw99(a<9=@?IWsdWm>ASlrl6I`kuZtVg zSBR@%a8~Gc$&T~7us;v-SE#NoHj$l?RsFDdvtAo-3VSxptViD76!@i3Anz=oacx-y z<`*^lOkTK+2g$ZuQAeP8?6;Gg$DT(zFP`}Usk_sc`J}w_vFB6pa^;SzI$~p-6%>Pc zRM@=;I^7y0f#$1Yv(RCtxP7dt8zaXDDBgR}SBGbZ+{-$xfM$s|JvYAFmn#81dHzJa zI$ZV%%7US7|LcbXdhN^?w8r+e0lH1hO!RS-^*RH3g=HbSL$9d`^u#oZl5-}bU91`U zU{maOg>}#YG*i*Jr=C6K+!i7^=3J$FgntbtLY(bju`cbTO~0C%2k6CrAKF9M*z)2| z%BZtPo9t(B;P$69v&Q$42f{Jumh)4(0>}lTs literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..037f4d0e012491b862b60515211fd19adb359151 GIT binary patch literal 592 zcmV-W0*^Jui0dO;*b?jdWvO_ z$76eDJXz@&@s{sMxmIKF=P0>cU+H!YOo4 z)TZ}F4RaJ(){-9gO*G{K(Pt^)Rer5n;LNrQ{HWDk*MJjwQP4lpx#JSzT44f-E;lX| z^j~R0=@P%#F?0Hl0iEeOuv97Z&Y<31S&Uvdb&NFZ3DsQa6!z?b;>vcB(~nLS^fpe6 zAbmP`wPyuShn)`GFAq-#mQ(53@mbRkgjXRTKC_K|tJ$w{SRe<& zTj6U6B$ItNX4`yVBHDq@{HbK{S$ss3$u44R!Xx2F8t5Gw;F<8)$Y-{(FWU};YgvCT zP(XMp{0!o6Hh5RqDe>WI?wAd2-H#e0)D6mnxiTIMfA55w*>)PlcFTOC#vt2O0yJlQ zn<#WHT+ep1Kp){n2#VXco9%uD^fbi;<}Bw9#$iF-2fYWBZ1+?^k88}C%G)#zqOomS zuG`NxrGTyu?-_SI&Ng)cO={P@=2+RL0O&32mmc@Kn{Da>diwlIc;Q&trT}O%`z)A8 z)%SV^xFW*mpkJQc_*p_+1&6b2yC_6+9P}^4xRcx58r&$dvHWpRvt%4{QYo}CpmuFp z1lo|)5hxz}oorJV)G|$KS3dTf1&b@6MPqp+S|>rb+Qmqq`0CioHuD1d>hRSe_p{AZ zFmh<6=gxooawVWA&u7Ha;i67Z77Tssmmdyj*_jKp%G(FsCaz4>ILvmH0WD#fh%V?k zHG-a)LQ=BL`sf#Ph8nyS>y@w$3P4vXiu=;@P8r)kB%j$StwpoM=QdWZ00%Y!@FN1J{6WSz!=$)_||jqf85gwJeS&Zl%4(1t%7BH9>GyZ*3h Z8s9MKUV)@g6Po}4002ovPDHLkV1j_)Cy@XE literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_help_black.png b/src/main/res/drawable-hdpi/ic_help_black.png new file mode 100644 index 0000000000000000000000000000000000000000..8b332a71afacdb5b4341d8cae1b65598fdb669ac GIT binary patch literal 532 zcmV+v0_**WP)nJ*mol`aYbfyWIo- z0nWLD@V)Q*S$G+S;S0oXJcA56Y_5w`Hb zew!eU-Z(%jZ?2Xkb7H4V(K#b&&oj%J@X2vZC{+f*F;{FS&$?1#@XdBvpx^9-9G=*Q zQL3(arf}-q0E_H{^gEf27mzDf@N?C=n=rKMsB{lQd<`skrsPMqCtu~(N#>tysHv-f zYOx&iqvU&*6PKWygeW{Ej3Q!n16dYGK38&0UC|XpII%%3sX`sgnTV-#h#(GTHmD)m zO*538M{hIh93rGL z=<~s`Z;c(@X1nEl^>w8VuTBtiJyFF@QqDU&QNGdDxN!n{u^qyEv7Oo2HDr?X9^!x= zu-$fsid{79>>4tOKNEA|#qhwF*xNN^a-4gpoxUn|4Vl=ngWK_ybh7LxNfWzU6iyx&@9oR9!$x$I+8~g)ZQvj(*6tuy<0YNoi0s3wLN=* zT@Zox;JLTFxSzM&^PJ~?d(Lyt^V|nOMMXtLMMXtLMP&uJY)zabg!qGTT=xdyn_j^E zzyiXtBmlsmEricRF5j*^bclj!cB!Ngn)X>@r8o&$>VwD440aO~A0 zR9SV3_%8b-fpDDs-Zzc!E{!1=O)#-(&Hh;r}0qqF=;(Ng5H5y zgkx;P0f2BUfv+!);ifN)`RHR})M;cBNj8B7C&J{tAMfJwy`U`iBe5hp`=;Oz#xOV$ zA+F*aSwxb>Xej!av~=9X^^qWTtf7VHfW1x=emb@pxf1)8#IrUS2**k5c`vRG%P$T9 zBnfckUJ$Lnc@YT5na{|4i~Zg=joyI(YqM$T9SC4|WiE5OOjppzRFr&kag1p|&XjO0 zfp0I3!8;vgx{8L9Yt4v07L!=b)cV^i{aWZyQ>A0 zV=+kx+24K}L;gope*z=f!mc{oD^5e0HCT9S;MVj@tkBv3u?ql{ULW9YUFtxv1LM4&Q@mg@Pg{s7{M%zxo{f}or7 zmQL%_YA_L~%1ReghWruq3}&A3_Y4N8FQK$Ptp*c;c9iE*-BXeTIM+3bp269)&-M(? z;#}7#Rm#D0i$1Y{i9mLv77jDrDKk2=fVNIAF5H<}al6G61Q+^e(AMdtPBk2633el! zEyk=wdG-%4`Qfig>T!$7Y7QD}%qU-ytakU$V95VyT~)~s8jA7xI~7dcjafFor^|yxJt(SVI5q1-s_yaH$NZ>i{-B)bTi_AZiWQ<B0W5{7P-v-kEhlGSCM=dRN4B2BvIhP3kh=R-vEXm z(ig}w!C9!oCvFEMQNEr6xIKAAp23%R9Ao_byivI zHq{j{x4RtK1~l$j-b z)l>Svpy-g?Q5mc8~Yc=R-i2kGS$)I5aMh&`o}}$_IPLm_N%F= isHmu@sHmu@EaN}#Bl9Ag)W|vj0000D4)3*Dob6k2z_R>V z`4*=xn`!qJy!Jhnu*qqP%|bb0?UV?;1rsuh7+cOv<}N)^Vj28jY|CWDtjeE7JAUXj z#7wi1YkIQkntam9Q2%(2yUUqQv5J2B6SAl7joAH1OsYA#vIdD;egfUc;OXk;vd$@? F2>@T;Q%nE= literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_close_white.png b/src/main/res/drawable-mdpi/ic_close_white.png new file mode 100644 index 0000000000000000000000000000000000000000..81f85b02bc3b244b90ef0bcb0a88888257787fca GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj{hlt4Ar*0Nr(EPbpupo2yP()& zGxL_qCCjfkI6drXbo+hz*MaqO-nTSp>i**td|KsYD}7GjoQK)tHQg~5uW#-ZnpoVn zne}<>lSdDyHq6=Vd+eyT AH2?qr literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_delete_black_18dp.png b/src/main/res/drawable-mdpi/ic_delete_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..090e11c89090ee32af60bc4f67aa49f2ed05933a GIT binary patch literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6R~NK=9LfcRi5e zEbxdd2GR*&{2|-vJdn}o>Eak-aXL9cBBw#He~utCvorgoS^s%>ctnBtd57T*##N0H zZyMH2WIemU9Vj%{<<~PoyXJR>1_m+72?-sl2`7?PM9pCX@(nbA#0~+27aF@5geTrp z;Be!W(qpStaJpYz!kEKZq{qzMYzD+ke9m$})n^t@^Uq*sSbJ9D`OeTI3P8uGmbgZg zq$HN4S|t~y0x1R~10zFSLjzqSvk*g5D`OKYLt||N11kdq5m7%c6b-rgDVb@NxHY7D SeESB}z~JfX=d#Wzp$Pyu_-A1N literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_delete_white_18dp.png b/src/main/res/drawable-mdpi/ic_delete_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f2f32ff75aef1789907dc378f3a3fadfbe2c26d9 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6R~NK=9LfcRi5e zEbxdd2GR*&{2|-vJdn}h>Eak-ar*5fL%yZ}0hjv%{7xA!Ef`y`Kai7i2$1$$r$8q)e_f;l9a@fRIB8oR3OD*WME{dYiOWrWENs*YGq<-WnivtU|?ln j@QPEU3PnS1eoAIqC2kF~L)IJvYGCkm^>bP0l+XkK(9L?a literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_format_bold_black.png b/src/main/res/drawable-mdpi/ic_format_bold_black.png new file mode 100644 index 0000000000000000000000000000000000000000..c51b2ec02a8bf7855a548d3d6272ecbc588aab0b GIT binary patch literal 228 zcmVqyT}FtnzW(wROt2%1E9na%XM(E`r2#O?>qT^w4FbG|Op@ba3@9HAUl5r`}zlKSa|2W{5k!}l!gDc{mVIjikWdH=iAeF+)phz RJPqhp22WQ%mvv4FO#o9oRzLs% literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_format_italic_black.png b/src/main/res/drawable-mdpi/ic_format_italic_black.png new file mode 100644 index 0000000000000000000000000000000000000000..e114dd4f26e711a0913698083425b8491321c004 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjQ#@T9Ln`9lPBP?bFc4r_yY_GM z+6hlsD_iO}eC5yF`gMC;*8#&0fqoGuHZi5$Iep!SFZ|@a_xrW)nef9-T|cuTJPiMH zKEI=p>aI8C^UVD-4F#P#Qj;v#e>r>N$jTUn=bigFXI2DW&Ij>#S6!L8uX7LI)>$t1 ved+?X&hxnMvN=BZ?e9Q@VfSBddCd0Ze#|d_o*g9Ue literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_format_italic_white.png b/src/main/res/drawable-mdpi/ic_format_italic_white.png new file mode 100644 index 0000000000000000000000000000000000000000..12509b5199f98fd47b268aa4b8e455dec2285acd GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjJ)SO(Ar*0NCmrN$Fc4tLJ;0W8 zm~D-6?h-!k4SgaDSZ`deWRyRyaH#T$VoHMirqe1c?F&CkyF97k{+_3ivZLHt@$g^u z5R`{(i$0m78AKZ>yHSwskZ+z1$%#d+&bB(mvVZ@{=0J zBcHq6eQovssFwOVq4jfScr7a0q;oTlVUenu<;Q6|o*ao1nl$<2#ts>)<9a&k8j*h$ dbo_;k`QxWVb(Y!&O$WMy!PC{xWt~$(697;-MDqXu literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_format_monospace_white.png b/src/main/res/drawable-mdpi/ic_format_monospace_white.png new file mode 100644 index 0000000000000000000000000000000000000000..717ee81ed707589a902185792ae898266d7ffe51 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjrJgR1Ar*0N`wwy+P!M2Qd)REv z;aN$IFJCGhY~9dtPxe_kbKRX3wqUNNc)^Lc&L920AU}7jzT~IsIYu{6r*C)lh^u7( zGFNZWROa9#TI{Pj!uhsMJluHfcenYXNzBBIc=Nq@ZgX>UG@o0__oidt9Sf$h Yce|N4YV|1{1v-Gi)78&qol`;+0B3AP5&!@I literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_format_strikethrough_black.png b/src/main/res/drawable-mdpi/ic_format_strikethrough_black.png new file mode 100644 index 0000000000000000000000000000000000000000..8001fee5b27b91a7bf3a5ff60a2a0a7f62835f50 GIT binary patch literal 275 zcmV+u0qp*XP)F~0VE>`*GQZ)>^bXm*+@h^?Y0P!wJH;qMn=kS>6apyU!*S! zj7mlMC4jY4%!qU8%LdCLGPqY0{SC6_Ev9)t&>L$_^A>}>kAJd7!m7KllLT6d0BbiP zFlql!u(u-tMH2ot0YbHD;G0BY*sSVa*S;VcrAW#srq30DH<(c~1Ch<<-cLncYP)ZkxJpau{C1`tpI7yto90G^g)Qed0j4EHm`(|t4ha=F_i*K3-l zQVM`pG>RtCbJ0z767Br{B*b7Zn#nk%-k*yG>#FatPE;3KpOMoE>}t5u*2Ou=z_BQ_ zDp#dX(Ba!r$FXQ&*YZws%{KZQRBe&G?+3pzSKc=nG3iAQGEM&%78CssR2u_XJ5qwt z$A2P+1~Qle@v;O8*3VQs(Kmm9%^|+`yypv>MVut0vp<&vY-H$l@m$gmtfL~Y@=7I% Y7mAhiK=)pzN&o-=07*qoM6N<$f+8w%f&c&j literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6946f51ba23cab8cf776d511270d61df7c43aef9 GIT binary patch literal 398 zcmV;90df9`P)-`)0CT3NU(z&07rlw?D*SqDEtw-ang3kR2_*YJt<>5cIMfM z$UnokaWZVZz>e}ld8K@oQh+tKbBoys2g*k&N!Rf~nevu?oGL#hCwNiLl!wY)BWrB0 z7DMcbbb^z6@@wT@?XH0DMoN)Xc~<~ET^5q}=u3$mDVK!HLLirC@X{FK zMZ34}K_n?p1si*xiVx*}?OqQaE-%8bqc2VL$Bs2NmxbiVzdJ`DNs()a$kKiC7U)US s=90(Hld{H!?@!Igzza-<$qS6e53nPI-6`$h`~Uy|07*qoM6N<$f}HKLSpWb4 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb72e994e4636107a5ef30af1929b40c88f8a01 GIT binary patch literal 420 zcmV;V0bBlwP)2xCBAv1e8VLH0350670$iphqY>*a2nBos38Jw66Q!Ye4Twj5Ja-{w0o+ z(da)xM83hS_lDjJXtERKjq*_874XWm~p8 z7dzFX^2>>J33i->?V3?PfY@ZfX&y8r|8PC>Jr%iieL?L2vKz%o(6HN_oLmOK3)j}v zwrB_(2OCG0v<+V%I1SSxLYgnRh+Sw-4y^>gs!QgWV&vkOqR3M0F@ac@ zV6PriJQ_R}IO6fNKN^R}@S!~&jls@j_f;UU_N(|*o@Jj#sN*yg!O_uQTp~X^tf8?j zNPd~~bVBSTY*!c2mb#s1i1l}-TqxHeDb~+bn)zyewC4TBf>3w{91EK)j7s65;N O0000O;XV*zAA^1QjfLrTS9>k<*2+?E+ScD`;0Nf z-by9dxfOp>E^~JVe9w}a44L;Apr^?~@fv-Ju_nk>%;7*ymeS5Wh&8$HoG$~DLD1;z zHF%VeyK(jmHi{f#D^?P6cisW~=rJj+Ajj7^!aHVfVCW{1q5C1*m@;x*#*|CKWg(Eu zQ@k`0rx)!deit!FdMZdP{mJQ5@uj@X-P7RVvL^gG`f`Z&*s;cDvQYfyklBDil3XPs zOZCn@(33;kNEkmW${HKKKQ(WGHRy)!4VtkB{Uco7naklU00000NkvXXu0mjf*fO4n literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png b/src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e310e3f18498f6bb04fc36c1b8c3df13b68b03 GIT binary patch literal 376 zcmV-;0f+vHP)E0bH%nq7eVs*ATok#I!aITsFbKpzXq*2W`Sep4UD_)(id!h-)RrXl=pfQ%>&K1;J zH?EE5TUfdo6m2zE13ZuJ-D$3cbysnzxfcv`(HJw?QTvXG8(} zgO427lvi$)YsyE%$3h_=Px;X}+!GINU-dxHSof=-v3L^Zvf>}$LNylw;W}%=qjSFe zi1D-Inz0xQi{EOUURXLwjH{0v%XK?fSeow!+<~X8DA$ZJ*FW>0AR1_VFpS>dH#h@Q WHFn@3e?b!f0000cwOv?w2Ut7v3}t0y=56s;)lz!IZ3+{dbV}-;&-rmSo4-ffwnt-E z*LB}C%@Z;wW<03A7$lp~-AyB+SzX8yE^4$y%Q>r07CCcYrHwln?T%dan}9@3!pOK) zEwP|Td|Y!0Hm}BFXewgSf~Kw`=oS@^3B&5s;%>rmsUBu`LEEV?VnKmU><95pg%N#) zHlFk!G_PL$Ipa5BjvOU=8~678(Em>LFDl?|xMU91zvzfCMr-3$|E0uB{fjQywHjAu zFF9({Pnc~}kb7ur89p>XWlc!wI*q3cSMQ)xG^w@2@i-@Gnyh=^BrI^a5!-ceY{yaK&<9SVL`X%7l)}NO3#pKb zN}D4=NE*t=T|mw=5_ky!>IZ#zyM>8Lq=>PQDd-u9Z~C6!_$S$l`H;1cj?m%TbiJ#CN} zD=d-yIV>`}+HFWppFERItn2_}>j(tuFM`6^>v0M2b+jA+;ti=l`sT5_EVl7_4TXXBUxwOr+J$cv$MXpn)Rd7GZPKO*=xSeFqnO{!g zo0SyYf9+NxTY}m^t9?4;zzkT+e{Vk; zK}dik5preJU-!Y*2(mp$L*ghKeqsL{|RfoC+0Q)!6fS>;W4)`s?H_wyk7SLjn z$kp?`Fe{C_GAk0Up6^AA0k?o|0l@x&erL4tQi_KChJ9KzL;wIxz39bo&|Y`maL|sa zm%K)WHBNwHlF0OU@SuUy!fjPBJsvcqbRJ`v000QM%;egIpk|vPO9a<01QF1A>r`W? z004-DoFuAowMN5!L_$u(|Bo#q7ajr6TPQOkGjBq% z2wCpFibhOxXDK7W(OH|bl%D9*+D^WpDY315VN_y!dEw{+Bl+!1Xymso(UU*oGpZ5P z+*yJ}%yRb?W`t(mgy0eJyoG=eKx9mT9E{9T5iu|#vr9hxw~A1gK*$&B5@GTM8Gw{8 iNM^V^A%qY@!2AH;(Qc%nVisxu0000VUekXM`jb`LS$AKh{cEu6BYr>TPPzUBX3;k5$c(p zibhN`vy_%#>zmA3N<;MS#FDQlOT?6~yp)J7fAQ%8FY>WVDCJ|8XvoL^8Py1CW|q(+ z)-yYW5uuScE?7h?Zy_KA5E(-t7bCM&cr5hDd?e5PTP4Xk2%>~pz9LE_`5j6r0h6zk h5=q_@LI@!wGJgrSxvoV+RT=;Q002ovPDHLkV1fl?ZdL#Q literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_delete_black_18dp.png b/src/main/res/drawable-xhdpi/ic_delete_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e5d0cf9156d4b1402d5ba695008770af9790ec10 GIT binary patch literal 427 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6Xz}5igjtE6@fg!ItFh?gFHN;HUHMdLYGF z;1O92q!YmSL$=d-AY-kki(^Q{;kVN_@*Xk}aFrJk%yF16DWTkKz?gf>XM*F38H#Hh z=4+XHO%`!{DRcW_O3WYiottX@|4b2QclvWI@%c{S0%oq`|2VcBP(P~5^?=c5-dC1$ z*B+KS6ueorovB|lO3Jy+>89YX)9>6Wd#7HQvWoHMUPVq%4=15t=0cMuw5U8uFgd}s zN>J;Ik-#gStTi!>jej@a_^|e&YTA^2459&UanH^gz5F&!R_M@C9#1_h|ED56y+DVn zmbgZgq$HN4S|t~y0x1R~10zFSLjzqSvk*g5D`OKYLt||N11kdq5m7%c6b-rgDVb@N WxHY7DeESB}z~JfX=d#Wzp$Pz5^NRTZ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_delete_white_18dp.png b/src/main/res/drawable-xhdpi/ic_delete_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..cce4124239b8e1d1abdadac01623659bc6f7a93b GIT binary patch literal 433 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6Xz}5igjtE6@fg!ItFh?gFHN;HUHMdLYGF z;1O92q!YmSL$=d-AY-$qi(^Q{;kVNl3LbJ0aQ&}jbb)c6R1jNh0HfJ7E)A6~sRgVy zTC+MUIZ7tkZjw~`_Hy^%{PK54ygZHge?Jx4Ftub-2(Qdb)!4Id_ExYvux{}wW-hz< zuvdA;X}(5l5D>k%za)TbX#f}R1@@Gk zSrx5vj85Mhq~aT-UZlNAk`1WdbZ526hUq0!N;KX)*Ia43Is8V;q?8tZ35$oD!*5NS z2z0(`iEBhjN@7W>RdP`(kYX@0Ff!CNG|)9N3o$gcGBLF>FxNIPure@s#VJySq9Hdw aB{QuOw}#mvYmNamFnGH9xvXGh2vLe z#E4lE;D_T^hbEF(m(aT`%jLlOK?vWMng*5vBy>{weB8psVW^&ZIiSJqaA29^Too$vd8&w7=9)L35~nkbb27kR{lNl*PH7V}4r z>juqOB_TdJwhJ^;A_?)u@vB1{{>BqU{+b;@v?0@pxb$vB&&US;DERF&!j*Lo0 z+8f%L`g)gJ+H`t>=>hF6O6_=HD>i*(FDLv|2C+7c=7Xf?ygZmvu2A#)y UN+x=S0>hNS)78&qol`;+00N(xiU0rr literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_format_italic_black.png b/src/main/res/drawable-xhdpi/ic_format_italic_black.png new file mode 100644 index 0000000000000000000000000000000000000000..9c24a7c5d91a6b411b8c4e1a645546d2ad39bf52 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt*PbqpAr*0Nrx@}zJMgfq48FYV z8DoL(zXS3=m`!y{Z~ZYVpR_tqN`2yaC+kaP*Rnn+@;3`OaeQ>2mRk8&=v%9^V*U+r zzScd7@fISV+G-yLoREeHUY}bzkKb$Cl%RKWeBDLoYTOI5&w8HY^=g{=soUS=fns+* zc@*CMck9`jKs9&2^FMlf-O}Q=tXI|HeFYj%cz+*SIKli4ce~TQ4f--%l~BPJ gpnDaMeBZ;kMj^F%ew*<$ptl)3UHx3vIVCg!0L5>D#{d8T literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_format_italic_white.png b/src/main/res/drawable-xhdpi/ic_format_italic_white.png new file mode 100644 index 0000000000000000000000000000000000000000..58321df80657064b360b5bbe093ad6b516b808f0 GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtXPz#OAr*0Nr+D)ocHm)kUclm| zz|f>@0z^&(e+Z zo~~1SOC&_5R=+gR(E5HUF(BmKT)+NXl0VCyIexLG)F9Z=x%^-860K65I9z@5|ininCt$yTvNz=`9d arT!zuA`7M0S4Dw7X7F_Nb6Mw<&;$Ue8Fm){ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_format_monospace_black.png b/src/main/res/drawable-xhdpi/ic_format_monospace_black.png new file mode 100644 index 0000000000000000000000000000000000000000..fc846e9527ff4b8cbb1a6245df260966783de603 GIT binary patch literal 289 zcmV++0p9+JP)iOBE}Rw5%jxR=Nb4{jw^ga>npmEpluqF3O0XLbSkrp|GzfLK|=93EDb za0?HaCEUY9Mv1WS5LqHTJVca83lE+p(!+yCiDluTS3+ibZ)9*^^yL<6IXM_=doae=NE$wc9!@m7?BUZ-4)Cgn1cfo-6pWIRS?xmF@rsHA72)B619N``h z86(2tA#y}`JVcC0iwDmU>G9w(Vp%*GBX+)LG2R!x7m6Z=VwY literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_format_strikethrough_black.png b/src/main/res/drawable-xhdpi/ic_format_strikethrough_black.png new file mode 100644 index 0000000000000000000000000000000000000000..e10402a1a57b2e7285e9d7a178c2fabdd8a53146 GIT binary patch literal 498 zcmVntej`xWrfmMoQs{xa2z?pdGSSFP`$5tbez=>l+V9aw}SAXExDqz*%&YPR~EwPow z-QDCo*aY$v4s6~R-n3oTcV#Q^kmxP;mxUYXE%uj%8|W?emxUYXE%w(Gw~;%<-W?Fi zx!&eLBG@mnc?d5+j;{hv5J-Z}dr8zk6QC3B*meHx?eG{MN^Q0vF$1`} z92Py=?M-O6)0kTD(b) zq`3S_p>uRqFqy~5l2|I-3~s*}SU%ZMiEzkovV5j6KlrpL@pJQGlLO1(^!4!;8)z>Y%!)J_b7Vzdx(yJhmxnXr(%m;hc7pC`3)X80K!(Rkh?f ztD(uahN-c-N{b>i>4Ra0g=Gc38_$NqeyO{hdSh$^g4rh{9vl4Ch!0xxm||$MxnUq^ z#qmQDMVvUc7%*uVfX*DtNTPU-Ek>e%Bga~3+?z2LT5xO;G||#XUf0CENc?2MiIUt0 zlR&yF1DhXeCoRr*XDS#=qN&(l7M`HB*k2Z&ptaav7M`HB*k4uTyHkt3JD?4Zn;d;1 zXTv;n;rJp*cZ~@5{GqlSYta|M=7Y-j1V~~ehHkzNOMEHSxq>7N+;ovM#s$^6dT64( zksQiirzO#fa%?dq-VZ?d{$nl${ryq8CZ5MY+2a2ciT?{hFd8;EisI+2AANLDq}$A~ zBXO+uJbyLh{%=EQ^2;BeDdcyb7SR{iT{bzSd&#Z)Vz_a~(fjqnlgK7_9LI4S$8pB- Y3Aarc5b7HbX#fBK07*qoM6N<$g7>M{e*gdg literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f2fc26df665c8a9d6cf561b40264fdf70d750d23 GIT binary patch literal 871 zcmV-t1DO1YP)G3|gXHi~>dWqSs{X3#uDE%$9o#{s`Kwen2p zl&9;8(~jRQw#%Ow)typ4|H5(27xe@-u%-4N_@W^o+o*9m+~tb~f;f3;VIRcFMPJkh zJ1tI?GaF~+71&gF`f3CB2{tL;AVJsW^CxZo`+qq8s>t(w#}l3|1!6)2`)Z)*2;E`H zHaXP$LoP)^+Q|24b9iduH*dq{a!{{m=ebnLfXMGU;2hEBCUM-tMd0~xpeO`|nRc2( zDoeGIKaqnj#*MJK3{;N3k=tSOKj8CCZj&S5v|p;~1G6(}b^fo<)>H-kLU%0j;jvs1b1qSi6-Fl>NfDbH3xbxaAUS^ zac2Y_nS=CK3bcHq0Sk$_(J!`YFp1QkfcR{EwMacrJ;N$Oo+%I5VM&X*RLjaQZ16Q0 z5V<@zh(5#?K0mtKG=WPe?rn51T}i4OE!%VjIhuQ$CbvitE#I(74bJmpfvXRSE-FXL xUmY~>XhYMxw%JPP1SW^o1eT4-Phf=-`~{#P6Lz6Td%6Gs002ovPDHLkV1i2Im=XX0 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4963c912542bac04fe3c6c0d1df35945d6e1b723 GIT binary patch literal 936 zcmV;Z16TZsP)##9inHVN8vLN1QX&Ps23%XC@~RT{R~Xhe|pwS^~BjX zJ+nQtv+Sb#l9x=nUsqM{R#(@|#Kai?C;;|&ZO7j6R<a~}o(s(J1LN07aCP~`xTf|826HxJd>#p;U+x*F z)nBVH2Xn@c5$7b+;+=8JxDvEefqdo|=cu7p*OMQNv+B1MgdjQ*r$3GR#+$mP45nQ$ z9)z`Ok7;>T*YpK5IF0x(&kfyHeJX72vhiEQF8_)4(h2dLe`UO)+Pa{PU`y@qs(^m9+ZFR8pi9=gh$xA|Mf zUE_D-k3c>@GcKt<5BQnTgT0};WT1G%?yDvP{@xE|C6eSE>-~(b&tP*eWX+Lkk^t{* z)Ooa2G9Y+;*MQ4NZFCbhzKHH0fzR`*>j`*c*5y<+84#~hy||}#GSHW3H&xRU@Xj%B z1c+OX)O&Pf+dM4mEc3yj^nw~(hC0)5b!do+EkvYJp$gksJ4r`=D*m$Xs3_@>4%s%Y{5Yy z%MS59(e*yW3KYxemTCwaUqbt3bX&BI$|%1cDyw2dw@wi0@! zFQ|?{{Z`^FN?uqHZzZm)E(s_$a|f!)0N(`viK=ws9523T@C4dw<_P$Ao4hf$y34A| z1AZoU;O`A*fqdpzH0Lwe{GH$j;&+?8LB4bh>V-y2@@K}N+FCFJYhsk- zD`oUzYXaIbz7ZcxxBIGX1jMUMn=kbmJQ=+s8UcQ_^HYa9VLDZwi7LQzUeks=rcc%P z1v5B{;tM{>L&wUosB6kVZCIlRmgJdh_rq2O_$Ffi1j(GuJ}t7JXFtP#j^s1jgKhXF zRaR<`F{t1(gn(Gf%YtY^+G3e)H*Kn5&Q{^yZ8Vs=WT|YY+SCR7p4dQjTEs{#*Q}%8 zI^Py7ka^>#@z8h@37TV^qo!GJbDq!*=={iIyn$ik!f#-VMEn6n-hzcMomE5t0000< KMNUMnLSTY1i@^&3 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c5c49cf7810621eed5478c39944c0f9aae4ace GIT binary patch literal 754 zcmVSCG6xbfuuE2wq|y#Q0FwM@~pp_2gt` zx_eGW&IdmnPS;d*&-A6bMCSaz{0$l%^8w77(GB2|>XB+o^-agtC$=Yo1=W4k zM@c(8O?#7P?#aH7nQ&Y6OVZAD)lbze8IZH87m{`)SI<>vq%zB@FOmjFOpEub$ExcS z+r^3a&409EgKc~seO8^9O3;ZNNrV1Ty;NO3G(DD8)hpS5Hu_^)E*+Ydflu4?<{8kM zvFplJ)o)4t@+FE+r=~f-tvY9odV&vpMgPyNQ4Z`j(K!K6Tcb=^CvU~aV4d8wMtwLX z=~lTTW!`)QpK&_^8M|Ek5Q4-`pPZ73+vO z*=4Kj%N6nG1ivpji*#U3>4F$1m><7NN9b}Hy$)<0v3MaOW{l+)9Bc{Zp0zCvoNu&}(u_9^wsDx+`#bGlPq!BTGR6;a}8vOuH{1l4JauMGUAG_5ej>K)0 zt!*fth>zWC8>M<+oeX)9kv`biy$&)`iFl-Qld4TqDsjPCbRZ^kTmx%UESUr9Hr5%b zE-7y*wx)My zrl)&%F;ht;o1N-cuXkT}zv-!|9{(xK>`zF2KJn-$FmBwq2_VUb;A`+b_)RC^zvHu4 z)av-L0EtY4FTu}|gop9Uk9h%3Yuu3l#?QfDk>uL)C%CThivebL!2L)fUHKNARllr& z!94giVotIwj=^nk)oW)w`I+DG88y`Ec=8K4r@mR>lQ@rDqzoq&L|7XRfrp~tFk3XEL_FCuaIPvk2ni`V=I@UCjxf-!t7jlZYbB$(T6G&l`! zt2Pzp*2zcl;5u1TZ7U$aEvs&o=a%7@S1_hk%jt)8IVa2=Qei+Yy5yR#gSj=K3wu*_ zjbYUj_CPhc0grd$d7_#|FxT6JfmoA`x5`w&W667kz6+|Wg}KHooWQYaN`YSDy``F3 znCl$FAa7Jt3g%Ai(ECU=wXkYSysDZ~fY%f8K2uEvG9boz9a9SMx*zfXhdQRo1ZH$h zDVS?r)W4s=WS5Y_#=D50ig*1YR#T zMb+ho#aoF7s%r$RHgkun$qmcZ%n|19ZNgw|b(d9_3y8hqJwne7)is8>i!0vSgaP@| zQR;=}w|ISL4ytVh1X!1#Bws1B9c+vRei9GX{xj7k!Q7=&Y!aRS3C4&dPwuKV8Rm|5 zUUjGm%PDzHn5%g$8}gK%s_z5@oU`f+KFLMLonuMI3<9-&j4oJ`YcB7_)-}vcBKA*^ z;ngDhdG<5>=SY5Ld$42fkQ#Kgcxt7RzlKj}Z%7g;WUO7wT{go2vo&+~HkwRZa;wa< z+q8wbb7Iqmmqm=!a?FNx-RE-wi7bFy;1PHhNt)mB88vCW%_yN0(B~76egfmhjh{e| ahWHDH&_wsLaXm=@0000uC81+U=BfPsAJ|}$<8}kvivi?dTTu>zyq5ALFT;6XpEP)l z_dLUnm~{?ec-jU@vOfbh>}RpqKC{0IGO#mYjdjJ>09*FA7#?i&;MjQDv{atq*okv! z9$H%mea*hOoMaF`g?K%?uv%xreKH{!i&*69i?6nGsN|my;n)QfYE}Rf7#kv(P@!ty>GS@2FS*hg!R_jYZ8@g{LYJay(vNBJZ|}V)V$4S?p;c@n1jf zmU_5{r3EfLh@M#kxd18#<;zGfkvwnz!f9m^xz!pf*r}IB@_0B56{^-z!4AF5OdhPY zRIo!2dFXnR2WveQ>=31PZV!&fa*M1vLkaT1P7!kTC&!bmMdVg%s>nJ+pIDL#?ABayzpI zudfk>V~sG-gOLvrC1Qp9ehJZb@-VG!EMTa5PA7}d>>k)wNK2*d2CBYE%d7#nqXc(9 zJHbbMeK+h@nB+$c8Nk_>BD{w+v1xm%+_7WFD!u`xpl?_rDTdJi00000u9*^XG^&oNkkktEdpL=rMqSKbO9l7}%# z&3;hRnIvO;6E7OvD4|&ayiOVxyd)pvY`u%ZU?fS7+3MfC6N$rnvo8TJiOZ2JKvo)j zBnfiNYG42UATf77Osr=wly7Kp+--<=vM~`C5^rO2Bwi}XNCJ*9#RMyY{yJCza&V|jHNQ1ttsS>;T1BvnS73ZXqe#fY-{WFR2 zQgn=VX=EgC@%3bZqDJQJjuetNiK$@yDgjCPJ^hg+heB53Z}E@7b)EJ+&^(Ir2O3EF zoEp6-U;Vozbj>SCRs_kH#1w)#_#f&aX=pwqR}xdmU~{=7m8C@TBryf}ONU7+x--d< z#H?_(IjSXLKe)d{0~o8W?w2H$<>aJK+2kZ)V^^O}3n1rU$J;~_N~-671D(Hr$*f62 z$Hw3_cPDt@^>x^-B!53*U<0y;BD{ysW63o8OQ(gUVC_V&3Z3;ZLf`Ol7LA$Vsj)Qnxq9gfFW&) zFs)ieqEZD(TeYgv(9jl2RaLDbB_SVLrBS0wO-nyi6cQp;AyoniEpEh>0;xG{jPXGh zUu%1>clrT2q6FjJopg=v>BFHjaoiZs7?P*otzh*$Kfbl90{4}>EmddB6egZX)0h9zi$5G2!02R_ ziw&<+#+D^vJoRt|f*#EhbvL~bS)I~9~`f?o}Z16&sBvaeK zRO88vPG5Pi4=)_P%B|N=hsPn~JC8>3%@yTvsOBxyidlx!8J+(8&=tITv=8Yl6>fhl znZ+;P>cO?)G}0NJs){)y!4v_K=`4Nbub1%pzXr`P|0x-r;?<*lm`r7n%;?l%f(XI| zNN05V>%aHl&&SPe{9E}`#~=b;)s&GST!8&2_68Mbl1VIybo0(NM>~W?2T@my*4hiYX}n{oz>}24|L=DSXz|HytzJ>#!n7(A(It1 zK7|Qz@KhY{{dZWDxq>;;IgEp+;==3+5g?Jw(w7eRiZI0trNE2t^kO_EW^XJ+fP<&w z_;_GKgegP#xPL;}DB!*enoMWu*M4^%Moz0Gnv5#HhEmtf;tx+`&~qb&M9P@cbXKR> z*D;96be1|fpOU!?aA9x~C$1z6$t&Vh@%3d9Ja&H>DtszR95MhPk<6lRB#k#u$MO3Q z`Y|xdzR;hzlEB5mN#=ZJg1Z2R&kZs6Oo;$3jb+%~7DL4>$7dW^K9>TYOTnB-B{nsM z@Z-OA;jNG2OlOTmDvQJChnUKp3tZQLbXKSDUl?J|XLEBHd)w;~y+h;Q3IPzErD1P- zJvKLoneD!RVH9c3TT-q9Or~|5>|vXJ7F3mBcUufjHTR-ARSCP+$FQKPgvs{Fo-s_O z`5qd%3NSdDW{PSl62Wt8V{mJ7-f2r5GM-x-1B+`Xp2!%op4<(t0(3HNJ)3L2Xo`6X zPTv&spt;t|R0iG)F;@Zl8GFx)1|J-GOg*>bP$fLn;A1L7|451{{Y-EbVCvdPq1;=} z?w!0h!j=bX9cF^7023*;1o z_P+Cd?Ea&3jCruuVJ5f=P+qDrr9aX+f+WKfkj&^f%6JfDt;0-k6`(%k1j}W> zmZ%8%3NL>ya1}t4CCrVwnesT*lfaMmcje_0m`v-~xxX8y7~LRqE8NiJB3^_wMIy~L zg`Z@n_)EtCw!hwqt3yfLtv^!$SBH|=_Qy`_>lk3R+g$5`#`&5GcX#y4hElxnb}!RZ zpi|(`nH%_b*BHLBJc8EwWr&vke>|Xf_+~udwGa9+Xh7g!kqB1KDP_)QCb(ZhiYL-^ z{mUO2>QixR5~}@y)u|WjRz?>1slI_ymqMOxt%h3@UktY9_0bf4`|J?+i?CBfd>Ve(QU%$>sb>n)M*si>y&BowR>O3cKUWlq zVAuK>LSAcp*klO-0ANj{9~&0(j~Q%O7(na%GVXSA#q=p?3Wp+*OV=l8$K_FOKWEjH zIq>~2R&u{;k`I;{0RS*N=ps8FtL8ElDH6f6t<{(vbXlhH0gDL$0Bs9`m(ZUA*(dyCpRq>}ajF zl&fJTied_y!sk@T-u4E}zQaxH+JF;#x6FahXwvFP4T(X0WtA`wEeI;DNXDNsU3R#>U3R#>U3R#>U2`i1{Bs#)?@|)XMq*0000=G& zhBV9dcUNhk<2ZiO;~6R4UCa?9QW?iHQW+;qq*@%0NVPa2BHayU6)D0Dw>$^*yt|9X zpdNQlu^ia)E>RYPTil^aa{O=&Emai%NDg(S}kMPf0DGeVnLaOT@r8Ci7Xn^)^u zxaZxav@G88&h^}-JO#LWmXDK-MA{}!))Q&FI9W@iZQ~>ay3rD6+ekfewu>|pXPZb* zanj#g-npK;gr3EF-d)1T!Xw|j(#)bW-@byxf)ZzhB)OR6Ss`dFq;aMT%|$fNHes=V z#T_av2e-USgvY=hcMkC!)bs8p5eAGf!@o}c1@Bocl4YE%7RfzMRz~uOla-Oe#L3b? no^i4?L%28q000000CD^R0J!k0{_j?N00000NkvXXu0mjfmF=u| literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_close_white.png b/src/main/res/drawable-xxhdpi/ic_close_white.png new file mode 100644 index 0000000000000000000000000000000000000000..f45eedf38e7e1280e7b82f2fd7c2cb289a0bd4f2 GIT binary patch literal 575 zcmV-F0>J%=P)yo~b@y??f{aBa!Wp00FbM=Sh|-u-7y11N9?cU&a_?j4{R-V~jCIF-_C= zWBtVEwLu1CiO+9kmh(HW+a%3$m?MXzAm=toK~4@yD{`t!T9I?Vq{rn&1ga_tcZhqJ ztHd)^K>!h|iX_Yy_vMj85JZH#R1#*2d-BO42qZ#YA_=p^{Je4qf{C!FO2P~=H@_T$ zfFkrMl5kecYex=2P!awkNjM|sv?qrkuqbgz684Jo?aCntF3RkZggxS1`*H{hh*Fy* z;hosuj~s#`qTEYKcqaDsCx@VrC@Cf3Z}GlgIRwQ-*(XW!iqHF(Lr_qZ-nvSh*Chuf zl{lwsc1kMo{19xERN}c|nBISovHtxFFa#Urj45fSoUtSwlrx4T6cnYG3m@%wAMQRS zp_nN9BxkXAEJ-LNN=iA4Gh;|X5mD}?9D;XZe?O8?K$O}fhhUF5*S;hK7iD(IA=oR< zw<`&OMTtXl2+oK(?MXsV5&k1N1ZTy(b|fL72z`nif*E3Nen|)>!k#LJV3wGlR}un= zkeA3Im?`ebCka7BxJ%^_%og|Mk%RyuR24Y{cZhp)NSa>%1h2&XSAo04{ap9`1P`o8 zDskV{tVp_74hfP~ODgesZDvWiZmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6R~NK=9LfcRi5e zEbxdd2GR*&{2|-vJdp9p)5S3)c|s@f3^g@ ziTj*+qM`oxr+^veT%X+%?`fQoZmnEwkT3GA_t2g|xZvyy8%`A*Td>#Z@VT}`^}~BT z8l><3&#Px$HZ74Mx_LuysmPvxO;OCL?2f7%9<>*1ZmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6R~NK=9LfcRi5e zEbxdd2GR*&{2|-vJOcwGtEY=&NXEUlw+#IbI|#HsH0LN}=$l|AYQAW3JM)sZ<|kYM zOP4KN)R(c)qsryeWrd|jBH!HPsfy37e7=2Y>BFgK&b(1rwnMW(^<~k6r?WdMUj9{R zbBSfPZoAEFzW2GD^(&J{4WWlGBuB~`zmD9#+{Caww7BlcS8vbrbJJqWqrPu3sa?=G zb4lc-p4S~GDx(i570!;|a^^?AIe&ZT;|*CdVZDb}6-Joytv;4`)nbhlLNJq8x$xMs ztx<>Vgbv>`K0Ht5tSLq@g4HKHUXu_V9nO2EgL$1sneV_&gPgg&ebxsLQ E05l)K)Bpeg literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_format_bold_black.png b/src/main/res/drawable-xxhdpi/ic_format_bold_black.png new file mode 100644 index 0000000000000000000000000000000000000000..2193ca114634f1c6f379f3a99d428bcb181e83dc GIT binary patch literal 614 zcmV-s0-61ZP)E|t<7Y-Js!FV&a~R$ze_67}RDRQDZ$nxjx*Pe zB#dbiChp0Y5^@bn|Ng<4CSl^9j42__kS~sd23_wORJcgTICd~`PsS`UHD}})b9c87 zvMY<6(?)UewK_ZOZ702^VHW2zETE>gHwt%o#v*DtUB$*~j=@4IX^Zv)pB)0Hm}*W7 zd8}E)B5G@2x=>AG0b3a69A+^2t=!d7!-^?&`dVc#%#ve>SItTMLdN8SaWT`W=wN1X-~$KG8CNZ8Pgn0*po4(q#1I@u^ijpKspIu$e2>p zpbRCiSbxn8vPGCV4G Awg3PC literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_format_bold_white.png b/src/main/res/drawable-xxhdpi/ic_format_bold_white.png new file mode 100644 index 0000000000000000000000000000000000000000..e95f17c547a9031d706700aa29674d1145df143e GIT binary patch literal 594 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V3PE7aSW-5dpql)?_mRh*2o6d zhz7=p1uVKbPs!8I9$SxYoj@ww-!V~plAxH&cV2pQW({4T6M?&ATHdU^2N`z2XxxWCWfgvwPn*%=B8 z1y!h~RcY*G?qk09-ulF^xJ%k+T3bZoI%it$DYUK+3V9q;aU=$0^&JtT?bqto{M)m0 z?ibUuAB+Aqn-~P$mwswxqwL#rsXwXMx6fHh@?l8L!R+M?CdQZ6rta?c5MKOss@{2( ziEPshj+f7H?utKk`{$JpY5x|k4^uy3tbY2#iiwA`4KE*()~|bHal|z3uw&P{BR=}q z>`bf^)MS>t@H+D)NjCoL3{G~jGeB|G-z~|4$@vPN2Z0_sYU21g;<$2Jxt-(L&x&VS zn7PxEmbK2E{*zy(UE+VrM3J*c&OVs4FK~|Drs8A&b9+SgF7c1>xA3^=z4LIClM`3` zp~a~`_=-I~F1C%F_WwIuim;uuYgQu&X%Q~loCIHz&3XlK* literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_format_italic_black.png b/src/main/res/drawable-xxhdpi/ic_format_italic_black.png new file mode 100644 index 0000000000000000000000000000000000000000..f37edb1d6ff24383a3bae4adc51a7e8890c9351d GIT binary patch literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2U<~kdaSW-5dwbo`kJ*vO?ctRx zcNBFmy)({|`#L*u zf00E%*^Tuwd{bndTSdJGr?`L4v-oUz zZsWGr3d!?(1k}#$*|4s+LQ)>YwAr){DC)n52PnD`B>H~O1Z&I9>smkFytA`MUUI(t z>stj|{de3Fk9o_z?i*+L@21tYhpz5>kTt(xtA5Y)gWQS7xRYb0*L~}h_;==X!m%9#fg8GKWa62ykGKl6i~;O__s1<)^Ec3*iZ5Oo_e@()8V?O1qbP0l+XkKA2hWY literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_format_italic_white.png b/src/main/res/drawable-xxhdpi/ic_format_italic_white.png new file mode 100644 index 0000000000000000000000000000000000000000..c4bfb299a54d8ae8469203bc54477da8eb4f5934 GIT binary patch literal 384 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V08C%aSW-5dwbO}*V&QB^`d}A z;{}dJD9O=qA%k%X!>N;V->h0aGjeB8?0x0Ecek%G|22u9-E)$PXOOhu$FH*=?kkGW zu)Y({$DP{#`&8n>HwE#QI#XodX}7m-I$Zm7LqOS%^)h0o_`Xj)>~!~!kMYXxjmoqC$lH^ffU&L%Qf#TUe_D=&#+S9 z*^!-MZXX{%F`Q9;ZmZzDZ(BcXdEdIcR^R4r_QTBk-2T6#DsFv0ba|iHyl;D-&j)EZ zw$lvAH|v_O)_>5(;`%X-`uR2-#m{26f$F87t_RwpHz!-s=56)GzgzrIEo&pf`AB|M h0foUNm6z{-vKa`y%VP22WQ%mvv4FO#l_hvUC6d literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_format_monospace_black.png b/src/main/res/drawable-xxhdpi/ic_format_monospace_black.png new file mode 100644 index 0000000000000000000000000000000000000000..78780e2f6acc922ef4163951600535310105d3e6 GIT binary patch literal 458 zcmV;*0X6=KP)#gW)y$^dm1wHubbo$jLUq9K~*V37@`MDy^(vmoGs5`sQ%dU~K#erQSWsL)yM#>%sHj8vkf%L3}^REk}vPatN*)z8!QiNac z)-G|N7Rmb$L~GYLkR_5!9EcvtH4a3L6e12pixl#WvnY{5zxC=JDRdn0ij*P_EQxfp z|Mr1eBz-_FBBhK2wMcUNmpq$}cHdIESLS@%xhdknxk&x3e|P~(ky^xoQlvC-U>>RG z2dK*A@b9vo?e$jVHR&a793mnjA|fIpA|kp%e`U(c4T~K(iU0rr07*qoM6N<$f<>Oy ADF6Tf literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_format_monospace_white.png b/src/main/res/drawable-xxhdpi/ic_format_monospace_white.png new file mode 100644 index 0000000000000000000000000000000000000000..54a8f104730a4728f9846a5ec7ce6bb58c3f68c5 GIT binary patch literal 567 zcmV-70?7S|P)9i9+&gLj~@lH(ZP>TL6P%O&K*C$7i6yT=aNKn^4#**{S{=1{5d3T$T>{X zhMYMheIp0wh41`^<^CBNB6oaA7$(N9&--$2_}*_pM#K& zrQIc=vvl*Gc5)IFiSlleV#F8DP$)(;mlPwv^on9JrkSJ|Sx$lhVpMfWF@u~0L&P|i zbeV7aBp4({uFvJZahPD37`r~N%Q^GCpMtx~c@w$g%SkX$jLvh}A6`f{B<+@yY)G0z zPC}CW0V>5Z{AoG&{P=egb+eRqU*ZL#bR+|KOg7w`O5T~Qu^4+Ua#}tmv+q^u4f<3VZojwSWbplNSY<} z9)8$&)gT}QW;E;CogBl9PV1Y6i5avOnRS#o&7aI)*mY zRnoDh)sV2-@fgBlC4b8S49Nm}x9guw*Y1?N%qB>TA3#Y1LQ%oN`uIae8=B+h3`YoAYR$d1NsY+JEB{*3q^ADAP9+M+kXQm zV?cOB_@7DYoJ%r<9dP)i`M2@zg}w_(?P`4J6ha&w?ia0y<1B_9w<3IL>0& zaVz3Di($vDh~q4V9k(KmvntZpxwQL{1;X|0r+M-bhwDrl8av}|mdO_&#-(5>M+M}{ zTJ=>I{)YeI8YBfa>_-S8gb+dqA%qY@2qEI|2^#F=1y0fDY5)KL07*qoM6N<$f+Q_c A0RR91 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_format_strikethrough_white.png b/src/main/res/drawable-xxhdpi/ic_format_strikethrough_white.png new file mode 100644 index 0000000000000000000000000000000000000000..21d4073526862bba1c3dc2901265755e6cd18bdd GIT binary patch literal 707 zcmV;!0zCbRP)buzqi z((a3w8hg{Qprx?cD7x2s{hm#NdMNd-m{T(Dm44@DK%Jb~DC|9*vBmyWlD=A!*MiRB zjIFICobRwYfIB3!OC1@Sxtdc=x2~(LL^Afp0gF#@!EMa`=A(OYOdMhws;tNyml3 zS-`c9^HRp%8S&J(Tdwf+ik*)00^e?B=n3K>qzS8&j`M;6Cdg9&?dn7DHR?&ng`y<* zY$UDn!G@;OG5k-9bp52X8t^qz%1LrHY_vUu<%;=Xp#DVcSw*k1aQ3dhPjxIYxLWx4 z4eF-8jyYuWPud>>>VI65U&FW;i3*c73OJ_^nnQT7-X#4*W77`Xv?+5?@002ovPDHLkV1h~GSjPYW literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1061500d83081a88d5ec974f64cd8ee26ca32d85 GIT binary patch literal 1660 zcmV-?27~#DP)dLpm`3X;DY-SeTW*OKB?$~ z1l&OVqddQb`d}{lN#r?W#J}r|K+qsz;hIVZuJZKM79Qfmh6=AP29Q zNkI-?F_VHEykaH=Ie5kFTLoQWyxw@L@loR|#&^|LOhca0hPL#fFJqijIL~;s@lNB* z#?Opr)mBE1^r7!d3S%)QWAABPWPIHCk@07>UwSwETVbpZjgKkF*mJnn_>l1%wO_C{ z&ln#xUZXvL^NqI{-!T5B_R9=KWNjI5R+z&|;zHx5@hi1oU@U%VyjNi^D}~F9FB<<; zd(xi28oxEZWjw0jaMz8OEBF)0-~7fi+R&Ch^!4L;{$YIHxTX~Ww*Q>*FSRG@C%&Z+ zvTrh8K<7fxmOeP;*A>R{tkt8-Gxc<>f=>hPjRIlUT|7mG(JIjNIq8ro(`^ z-S~;p3xzqdd;9F%g+VCqidOF#Z_qfCLdHSk`)bdl;VA_lHVQ}r$#eW9AghRSF8#0d zct9pfitQv!@PtNk+W3^1Q)UvF6+35HU?*X2F`tCfeqh#g7?7iok9gq_hm6k|!>m$3 zHa-c*?dJziYe0w1$(e(v7Yf9uIzCylU389!UFRC5tAE zmH-nytPlaa6yr%-&RLr(FK^tZc*gyc@s7E6Rlx3`*yV-6EtN$FUhEmgCtXVVKCLn( zaRiD*Ta^8R@#0z2{-D%JHO~tpGH|xaZGD~}RzKx5WDIsviA;QEEaDRBuA4RO59+~Z z)fPlVD@nlcIbDh6qQu-v<;J4!FAC1T8rUUX2z*odQ#ldnxEAv{IWpF(3Wwz`5YpdP zyFcU{R}I`OcmeR}e4_+7mJqFc-@YWeu|smeXZgO6;(s-yu4agAIgsbn+&v8$^9ki~ zm)d>dJM)c7;RnfaeY;mTNceoeT4dpJIG}dF9Fy=3b1cblQ)zzmy{kTQPc?ezbo9ymB>t$J3dzBat8%7Y&K>l+?AIW%cB+Ks4W!oQe4`VuJRB-Hs+JcyS`SriS8PFFd2&sm;=$_ z;H>HBK=!AKo9!r9&w%K#rS=3M+Zr3k2aFA5;v-|Gg~Co~pbNto@C#juA4+01$1pc! zAS)F>AQ7-6p{)icMQBguG^`?bPxz#vfzQFLVmk*i$9pu+V*L*|J3enqO{@vgM2#U# zjxmW=U8w?d;~UD{d2ukTDSRl|^?5FnBq!mn9RJm4CF#o;n+jv576&q?V_M=zoDE1I zQBv1rAzPi)zsYSD3AeP}Q0U7Tu?%^h%;lIybjmrbDVSTzC(V--fhqu{63%AaTOCj+ zjgn_U{^mD|U3~CB$%0dPEWPk#4w%)NRt&82A%%#M@(Dw-Ij`Z6m*#VFXwF&W99@2? zHj#x)WcTrLqXEA})_Pj)m#IyT56D^>f3pZMJ3=v^y-{YUVS0?gSR|vQ5Q{kbl?AIy zLdU-@kU&!M^e>(xeK_k;>|(4zN*2ooRXj?a8w%wSs+3&6cx5!?8Et4wANp3W1nvcR z#Wb9-1Fx7#K@MIqlY$()VkQMSc*RT#a`1|o6f|MN{=mO5{;RD;1$tos0000!^o7-vMLmR2 z8C3t%X#Sk%*ln=(KKq=r_c=%J9x&i?@0@GSJ@;C3&b8LQciq*aM~^!llKv^tf~(!Hg+MsACU_+H zb>RVK#jk>ggKONsxgs3e7s|v^tbV$l?4aV~EmlI!J3U2YSW(wuZ zAvksU24}AX58<$;$Pwrc#$XwJJ{a5W4asaHNb{LV%ACWps&bNYd`(%+T5s+cGk84s zYRgP^gjDsw{3)t{lQNMHr)@%a%A>SJafl}vXs|VtM`LD zX5kbzf;)pB6#N+S%iwO;?F7mukUqzqfV@S5bC2tF4QwidRGLXF!3&y2(pKh_Qc}hV zmemf|nH7{XiRD(xNhIx~uG1=%JqrDZ8;2z1eVW0tssgr^If=yW{=sk6Rl-*8%Gr3j zQOLAD<~nsyHiLVE&ncHB4@J@^sq!;=ZVMzW)u+AwP#YVGJnUB6Gj0ZmqZg!oip=|q0M;15BsLPk2T%u1Z zD_~tQo6_iaGhh1(AE~!CvPzL7(m7Q)%be~s)ASt&% zrD{vCUk$Evor$45>bzVqMrB}cRXg?hyK?4C6PzI#gPGLCCONZIajA6MTxVh^&x5ZQ z3{lZWHekw}Q`cCIOO{(xyQQl8YjCg2PYmTOaU<{@jo6hdtJQgpmvcsuvEB@BbbHI8 z+%LXY(1*ueeqtzZ7Tf?l;Cw#V%`lsU_F;aQiR7^=RSIq0$uWuMdL z>=}|VPrE%w$UIWeg%@3RBnpF!;&vayp=|hEuPt73dybHKu%K5b*H8?Z?~Qgvq325-1MhqC{2y;}Mhx8Q;e*d#!0^gdNw_yF}w zYy`La7!KV@NxBL+;)4v>q|9?<(N&p0<@OvQ^JqaAj$h`hpy?XLz3OI}EmWgewbkIp zv&mNDoZ6j53%AvncC*T17aL61pOoBE>~cE})<9w4;)dnQS$## z-pMtwS#{j;iCRAH;J}P8yGqN1yYi+7X5@ouf<$o6sKLRzW_e%+RDa}p*yT%528o%( zaw`S9ipa98m^mYZ1C(}3U3~jTu0VNc&u4FL5b`0xIEVJ+B$901z4|aP$RN85T_3Lm z)gRh(xp<%JObz9=5BbIoLatcGstU?4Lpbp}t?aY7S7SCpe&%=N5M*krl|%K{J||qh zH7J+J)q+vTLvd->=q+~vYzGghy6^f%4VUP|P<}93stha#s>4p#nH8`d{A}Qh``7)| zGf-BCrGf*HY0Vm+*T)Ow$kNgxU>DTIg<%QGyinKp5hUJb=gSQn16x%E1X2MDyDggl>|GYa}|pGD76 zmzUZmwy=rqiF4e{pv+50mEQ#V!?aDd57=6ad@~M|%Z^gaXKx%cw6XLUgRy8vX(1XX z`-Me3#Aanf$GwvmAvY0pGq1FYrDN8p0XTd4u#X>NKh($ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..13bb8ef9344f8c556df8fad7b18f07875596f4a1 GIT binary patch literal 1397 zcmV-*1&aEKP)v>B6viJV!{iuQ`AULIU@z+GCy;`&Tiq8yQDS|7D3A~-1wTUEf;6n`pY_5@EMFPe zix3op>O-}Dcif%hnwh;l_w0YCI}bb{_sq;%d+k|k&8)SR3JMCg0shIb;4Zvjih&%w zVTyqqykUxg9K2zQfgHSHwyl94GCpBEY&>o}YrLS?FctZYF^pvnb6Mkmg&oGnjL#d- z7{4>#RBVhUGKaaR71m-+)?R8nXnfoFmGO7Q54{!st+3Wb<68v`TWo-$4}65#tMjejXt)+@fDkg}gP zKEUMr1Y?;)Q2tn9Ex-OO(dB)GOgAXlIT)ud-k|IyoMu8_^TINY)38^+%hWO?}z-EgQk98=oWAis42{1+GU0^ucflghUW;?WO^^@Lved4}hd~Rl3D`0m}?DE3khI+~lbAl^SD%%pkKQQi|4qY6SI;rM) zVPppGRzn8n9MFlFnaa3ix&zapi-UUbO~r!9XuTc4wgBowsjT~ladl?g!eEzpA@Ebp zcPsB3r1bNO^@iNzS_7R0F91@j89UxptT&|i-x_kQ88X}0@q%K#;b*gh?iYTMCa!GG z8aJL)talT$@B^DjX>{y(M6upY%)$?BB6qW~10B8IP0Yd%Y+^A`1B;6_aAkAWxY25m z&S9C;{87bv!!4!x9Lhnl0lBLN9nW&r(DGhZSB=*c>)rZgwXWPjJ9yd39bc>Eat8%7 zd^XiBpjW=_3ud6M+{7I~A*WsP-(HFGZqqSFZt_z`CX4v-kBPFcuS z=jhMO}v>0xE-mO&)z6AwBdTJ!CD+fIYP`6>_-dUE(bdP z>jDQzN}m3~o5&pQdK9}@tCNz&(4dM(sdH7KJVKR{_KP=0MSf!pW0}L;)+>R_0B@Lz z1$W^MQw-$b4O0x{;0;p@emHYwrI4?QZoL`cMuEXCJZGGmv=)f*eKl3~OMj6Udhq~IH@`v+=vtRw} zMefL9=XK|=#94KG(_+Y8cka`+%0$_s4oUe+(U!Kq9ejDsIiziMDvK|N;8f)soW1Rw z#$j6^M^LwDgKqSB(YD%~z}H5Q`uUP9at__9%t?mhA1y|+#wT~Q>6~|dDr~9!$;+X; zb(@nc-sH1Zo^x=Fxz7o$+qB%qN#{mkQ~dzCBVTXrFL!QP4CPg$)yK}!E}X)KbIkc9 z@ngc5&broZMHZ7l<~iyF@D>Tq{aUYWVbdZ=*_T8YeAl{2+7>w_OUgJww>qkIx{}40 zM0d+_5=r~4)@f80a}?$yY8*(&C#-{RRVHj(^ z3g6V{ISTN3!2&R(rkTNYcLw-RHF46LnsOUz}$ zCySh!jkt_-N3>3DS?mYjO$;%jbu$60A&dQ?Y}EbLIj-ev%i=3hBk;b>u`2Ic7VE_i z6TjiSmai>~lLa*Zm$dBA8`C z_wmuh?@F|dEos3SIG}9|P444^iQkoI8(Y$XGcfowDSS-)u0-3|k`|nSF+fuoyVw-g zw2h(3noQDVvgID;pGy3O`G@&3**H4kz1bgO>9 z9F@geK5a2#l;wmr!6vpz$o=G;X|l)>>lSm4s=U-R*uo~ZYv;JmWRaK7THFcLhiMv? z57-(;zL`fB-HxG{Yj12bG|}~FgSMEAGKH8q*$-C!5C<(Lbo}cA6G*l^^}(k>9oBkm zc5PT2TNZuGA8N02#A3+i|18+0yqW}K0)L|nWvN5mW_JRYK}lflq2*Rq8kjLaA`Q$K sAdv=U43J0zGX_Ycff)ldYShZ;U+IH!FJ#)yB>(^b07*qoM6N<$f`d=&cK`qY literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_help_black.png b/src/main/res/drawable-xxhdpi/ic_help_black.png new file mode 100644 index 0000000000000000000000000000000000000000..4824ceba5a363f4d8d85e5fbc7c0c8f6ed9b8e64 GIT binary patch literal 999 zcmVHww48=2N?%K6k3s^p&U9&c8v)j4v&duN_dhaLthzWBd zN)?g@Tlehe-Qg$n;3x8fCuyZ%`+@x)9t3T%T5=V99Y0Z6_AOUTN};A5bL;uwKKmdq z9P|#lAQp~qSA0O7FuPS{evIk=+;vF*0yj-DLRE=jUVj<@mZx56`OhSx{2 zkFUdL<ncm8yG^w8;sH`O0=wPqT_JC$?9G z)32Ur=h#Y)2xIs;LEBNC#VS%3#Aw$j*&?wmYS2}6jRVV@jh1R&oRF8~tR7Kp6~Q)- zE!EZ*PT~W2WP_z*n*xtZiJupET&XZewwaBlI9rQj6L?&yRK=Q&?Ik~n7mf{BY%z%K zX5p0G%TgOfI=j#(RNl?J^QRR<-4$CsK-*zOm@C^(GAl2I7D94jUB+sAL6|GsPqKG` z-bxOzjIXLtPl{u_R;lDzuWrnheI#Kcz2t-aiYYO|b@%1!@Buyfww@Up6?Su+dx5*7++N*Rh-Jpt|nG z^eWapr7F&hyUkT(vwDPQ<0*LHnEg@?e~`$CTzX&hXrzRZ!7C5)C{!ZtA9PkXCWD9jKwo-XD}EH27|#C{{R!C V6VK=N*;4=j002ovPDHLkV1n-~<0b$A literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_help_white.png b/src/main/res/drawable-xxhdpi/ic_help_white.png new file mode 100644 index 0000000000000000000000000000000000000000..c6d42cf5c0e829d6f4fe152e6538ca8fc39c585c GIT binary patch literal 1025 zcmV+c1pfPpP)e z#QCi%m(abE=-S+Kp2Gz{rCycfwu0og&XkkDO9GtK))?PO5^xF3$&~Wpcz~~8S4aL= z%V+YPwBh4Q8QV!qKu)3rIq9G58gAy>8XZEa>cq#t598fWWlS11LS?Y&bQsT9Ea-3Xd z_G}XTy%h&3;bcK}-{0Wh!Y@DHaJyrPrxK?GyL&gZ-FSYmUl7vUg_Fy(cq_-`^1^;q zLi^~6t{gbsIf3Z%EZ$1{652-*+6tmDv8M=SQ$(Gj;;pnTp>5%Sp8{-J%GVD0SjZba z0d*9Kx6-zh=oC)M9ek@87QvoD+%5$lAH?k{xMk05p{+QZ!np-;y9(-ywPLmwZ=9P5 zHd8Fx`6q?;)ytAtkuE9przl^|;NL~D(Hf|uM}+njHgLz@Num9w(1JHaRv8#guZ5ZY zlfwE1I@&oR*ydMWsG+|O9!08i9+hKSc;^^NDJG-%bo}6af?(e{f8jVuvB4mubJU%; z2%S1d;8;nq!5}8iBM5d&x(dfjiVX%Za}Gi1+&KcrObTt!T53@14#sR)qtC3Og6%Sz zBKp_?V^-6uWqwwSdZFG?qy2eLE;GkY3S)0I)O#U}yGT{H8YAX@K;0B#+%#7&P*RFf z7dUPZx9bJR%@zh-L5>^5?K)9jpo3uW$n^|R7l zY&3PN7Yl=m^dliRjlLsFyw9&=SnyL%9^YDwD)A7n?N6mz!H+C??aFcJ11=tBd{s>)c{#Tb3qB00000NkvXXu0mjffOzR; literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/marker.png b/src/main/res/drawable-xxhdpi/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..12e633f87b9a58a01f3261a8b06a7a23511d327a GIT binary patch literal 3794 zcmaJ^g;x{a7vJUtq`O1LK&7NcNr!;6GGL^{0Hwnb(#?<%7}B7AsnI2kgmfv018I?x z0Yi`!_|1Rd_s*+x-uc{n-;HzcO)xUhq9SJ}2LJ$6I@%hKiD&A+MMgs0Hy{3JAs%2~ zH60T&;s_>lj0XUy6Ld6GO#-QkVl z^;XaMZ`#O3_bp@Qp5KFQ;o=yBZ1>V#y;0HKE+zb%{NjCsQBXcg`7 z>x5%V>*GK9W(_~<|L6t;7`-1UGlqV>OAj)*NhQjZ)FkNU^kioFBeoq_?iOL7w<*Rr z`eRlFq;;Y-j_L3?gKcSMy5yH^1#x8!t;{WXE1aMh`3{6jByWD6`nlmr3o>V7qiPa7Zfvg$tD$OzphklF0z z+8<|G2&#owjpm(>9S7kp*2=RkZ0KyE^pjJCo$~0g>v2uT-|YUY*_vcUBlJoa5hM%E z{NdqGV1F-VwyncmVY1mJw!!DZHhYPDxws?I2_|E5BH(K0q%S}yPeV4RM76p9Gp~f- zs_k~-mzt|z3I<7ITh>EaLT=R!*?%#_F zCD|%}W9EJ_AiCJ+sE{V+i!(ub5|hW4-?J9i8!zA+TOLD6SL9ftQWy|QMs%4VmY7z) zOYh*#7+$C(Kl?UvjP_Dx@*_k4lQ3>|n@zg}pCMPq>x=KKZI<}YQjY?|B-gq~_m-TS<#d$rFwLooKaax8ZFbS)0ya_xj=F^7q@kiq9F!L$F<*WOPFLN{N&*q z^7_m3$vr%o3hLEDzSPF-)1sWLOana<9q4GGG!lGLG})n)JnXoBBz=FzQ6kPcXiz+T zy+Py?G8~d1Or0Mexr}ZPmLlhrH_n?J=M%k%unD*m>w@iY=gAf1+cK@I8;_`V;>aYe z^ujTE8u|&ObZL`!vZTq%r>dNGB30HUvd7l?)9dO(mh8P){wZ$%a%sChq(GR?PkN3} zF7jfwYau466y4gk_bm?J;(9|&0n;z!TtbNAqw8QZ$gIj#@PnXC% zBZ7{;XeP<}%E+wQTiRS#6+_OhnbklSR1NaWIryE&C1kbr{DWkBs96Km1Jjl)YaW1s z)@e?5H$2?@%Kx5(s(-}|RN@$3l9QRC&61NpM8QUslL?q(ma#h2J}&H_K4g;?dl(D_ zRZ`5^!RKNc7Zi^uRs~gr<4DH@BArsQ=l(dybU2x_?ulT=;+ty&`B}T{vY?=vq>#8m zoKs|i9c*y^t4%fNlg|<(Z@(fnLQ)4T+`N~Sy7){6)<0~@vo>QnXQ zrVANS8rBs)kyOAqSm>I=t3xHah-ig-7?G8)+Grxbe0CjP3=(H>AkOV;DO?TuYE&W7 zeynHE_r>>?<01ZeoLj+V8!=S26M@wETo7Nr;`WhPl|M|JGnpEr>efEhW) zlF5K1kn;Hx@IK|6(2=2p#D2|G;nJ3K{3C=wjR=Vu@UcEv2;_F%N6bH`$G&#a?~7mZ zaiEb0RhuN<;sdDvf{~p}g3g=mUQD+WA#WRo2aY7A$p2m~vK9n{)$^WeRz7P~eU11)&yvy(XA=U!2(x7emeA8}j1eOFt0>SPmijzx zDC`OqpDC$A42*eooesqejqY;DmZ|Zk(NZBUeS}#oXNZ?`9P)0Oeys{8*GfUtZ5IxB zihC||wI-vEv{q6OqQ~8b9@b&$QB|lN>sNB#Gw}uReG@N-)Wrqld&In(R%d$6RUyKP zWXSGyo$Cvc>DK;-Z->AS%w~FfyhKl$RwPvFp|8hB^9VlDzbi>=ta~<)21E2=yFB63 z0HBIiIu(c75AhGYj#^sj@O}HS#CgClJ?{lBD4V+aw*jO(uDxU7n-X9U2nyGCr!JuY z2I83N9ULmu_wB9da6lhR{Vl{=^~gXYgaL1{8#_OOG#tHXp-$Dw9Ey`yTo~^aiX(Z0 z?&RXmlI6H)Te#^E4?r+xFSIVv&}>qTJod8G zZ}IKqVUFsMV}+;yYOy`&0$4Eh-OQ;yl#IuU*V1G#CBtn5-bKElF{*rdA!VSP8X)5zxLw|fMm;D5~OiGp07O(6}dUxq9Vw!EJmV*J>kcEb*<<^~2gRwF7k{{bRh8)v`VQ%pjJV zqc#UgufMUiZ!qX(msZ0EV4lKDuMu&TSZ`OJ?iCmggUm zKj8J?LINAlP6qfUH8<2_$ziu~YFv^7m9P3zGn%xl2mtK1=J-4NbzomGHmM3l zG+HT|yzBOaF2AARa)4E1jdeo1LP{9z|A;HhQjTLH9n6`F#U zaGg@W7B$_*^LgV#4xzZ<_Al#!{ljeCe#Ks=CL-Zh8Yv0IZX_=Z9X+`?H59ywP=q^D zff;#_btcJObOi&~opR;6Y#yC&nG3~cIXC;Uv&6`$g7Xp+WTU?r;&`n-%0rTSiLEk= zy6?;c&*%2-cN@FHcLoi2He5#QbG_54>f49bCxJ#^V6;Y7)EN^@TL)hB*N1Vh>w`DA zc0y+7x5YnA{d9#N06}OWOJYf^sF>EvbFnst!rTGR){iAbHA}r4SecfpHtkpLbvoN6 zCvwy19`;C&m5)Qm`Af3}W&X5>>xV-VQi&Be_Z*X>4|5zGLvS&q&HbJio4?$_2tOdp z<=2hYD?L1FqL@CNt8n0dV2#KmJ6_zX%0W#QV~O7Uv^i_YQ9`Tl3#CnvKNgBxK;<)} z>{9o5tm4ILB+A+V!0^G6RENzUM`K%*vGzDrrD{~g?B0Q#VEadBCHOQ++d?ceX;)p% zdd_*A6-*%5*Oo%Mk??>tp$SWs3Z<*}#Sn}3YRf4Po_YOnL*6-Hvq>WMdI-gsn zy*_hF6asR3`cPS^uH0ZS?V$V@$Co$0=6X=fz0baD&pNy$$ctnf2w!MO z>}1Sr4G+~c!Z)76Mt|aGDaU`pjL3uC4Yi}S%sUz!B#-d3dS}#eEdRH~qXpa5U4b^5 X?Jp!OHa`=aRDh1Afkw63)5!k;(QzAE literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_delete_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_delete_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..99a19985be79d722d2c363b5f5d1df33f04ad698 GIT binary patch literal 510 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?d}4kf#9d}?s_1_ zS>O>_45Sml_(QhSc?JeX22U5qkcwMxZyWM4I!d^Iv}bLfpfTtC#vNQw%dA?p7c39d zIN>ev+xN-eNt)%~XHP$yHp^GVbJ7w%oyjtj@7TpGS6Tha_lwl?^~zhjjH{NMoPPcO zF^Nq@^DnnQTvRBf~Q zwz>Nx70>!-d>8l5s+(UEdJq^WswJ)wB`Jv|saDBFsX&Us$iT=@*U&)M$SlOr)XLbz r%FtNbz`)ADKt$Bf3q?b2eoAIqC2kF=9^bwJH86O(`njxgN@xNA;-kFb literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_delete_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_delete_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ee979744465259bb7bd0a70873d823097020b731 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?d}4kf#9d}?s_1_ zS>O>_45Sml_(QhSc?JeXB~KT}kcwMxZ*Rhl*3TQFU-Q6x}5vCg=`C_-?k;~tMBws96JYsu$RjZagbDYbOaF;!%ERwg}k5s4q z3)2bv#+Ld_Rq5Jx-m;tSM|NqQ3-*}heaqw5loG8!yr1VyUUK*C%+K$BGBPlH@Q-0s z@u1GbWnduV hm@*kfLvDUbW?Cg~4Y@LR^nn@}JYD@<);T3K0RU1+uN(jX literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..74575baa032ae3a21e9cf922b3170545d1b3a39e GIT binary patch literal 1977 zcma)-`9BkmAIDuaN66Pq<%*(+#z$BYDMu8_^l>v}2p?l?a)h-U&6%T|mqkBw6) z7;xDy{!2tKCwmsN48bJxexroMk_jx%jS82}iu>4MVru|CLeiOB2Kd4FHOg)S%F$b8g+uIi;Er4j04k zv5B<6gjf)o{PejX>G0~2BJVrPOY5|BnrlLw=4wy-i&X=`DZ(zphU6nCdAPlv(rC@S z!{!g1Brk@Wu}f&%AoS3?7{9dssSCQCCp140x(B~|flQb1R*;EQTPO7zn1Gy4l}N}3 z0P-24F|ZBsj9j#!AJeq#oPddQOt!n1!>WYQaZ2wAX^YQAFuIGt9^G$mZHxv@K*N2T z+lhO}C@eMNvOt_m3{w3p*PgvZ3(W%$*uObH82Co1aWflO{=rZnj@+l^Yqm|07L;_> zZPYWgYw!-R1Vi2}Sxm3J7#!E{6%oaj z!A&&^4~={o$LbPzm_hOBk3iSjWT_EQ@TVwP`_7Do`raOqsOA+fR^VO!E{`@2YC&E` z+_r4BgJoByxo2F~XWZ`jIvp4qJwBV_oIfJG-!>?LwqwsZ=j%L1L_AeWFu%WCH#E>| zQF9{*(5Ty&Me6HkdpYhZ^CKS`RPkin^l>`}h&%3?q+7<;1 zjn8Vx?O<*e)*%JKLJwLfzs$*lvBrpWtutc}->2{1a2@G1@uN&0d$fL)$aFl)|8t5H z)zZQuu?zY_psY%_a6^?dS+X z?^aW%jG`IS|L3MY^BPGhebKpZY6Ej|qo8b7>-B1J3o61i>l@ya1>c^`oqqJ3w3U-+ zQ|P$n>}DtD#^rebSdwk&%!xFEbT{@diKUyq*3B(aCREcIf=~0H8sMa_5e$J}i$j#fPOS7Q15&Kzr}$q4o+Lq&Z46mEG*&i>1p*wZPIHKua!64e>H z&K{xGy$jcumvJG4jrrG0VQ1IGyTPiX6zKO%Y?XZ0C3}wL3+`}>(Pi(@*x;Q|s#JVs zud8K~a&U53)HJpAnaEwRe{m}e6=q@FWuxfs4VwHT^jZ#UP7_-U_p-J5nMMOPuoswn zt@c*VnX}=VT2GMB;XC!6F5gR!zy)VS^=)1DRVW~nPs{i4&-7}!OW+{${?_E}75|4o zYLYQlohDP^BT`mxtKF5*v75RtT=MkxDiMykRNO~+NYkH=Vo{Jdi@{lhI>32YE$mSgAe*Q z*2G|>G#>p?(+UzdML>{b=>w@2wYOH}A7FJDLKY96GLqiFeJxaDRvaiOi3R@~&>&#f z0$Rr~zGdFV=-AC70GiJA@`CewHm{>zX;QBAASaR}a5%?T?^-8!%*l4^igVk_LoivB zIEn4>#a~aPkGgWt<2L0{Lqcmz#~jcn(@5p!?%?j%ZdNnui)!4T!dKqm7|MH7Ej8IS za1CjX&kY_Yiw}wbaX*MR9+1nfq*Fm<=ZH#cg1E^t*?b| zfWKT}-N=vK*e~9J9;ga~j(f=~spqM;tf>K_uEkMP14qf{&fqT=&?Ylg@)$f+sbJEjrn=+`9WC+{VUiQA>!|L># z?nzGBA^o=D)p`oG3UvtaaeH{C8=bE9e`N08_Tm4b0Q(D&f5--r3$-XeNHU+5xvd%T HDh%^C_!8p< literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..9c147e6f859f292c855dd7c4e146dfed64fa290f GIT binary patch literal 2185 zcmb7G`#TegAD+9&?T}^a%*t(Y5V=JpB=_q!xs4Fbz1Wt+$fb)2A!(T07IPcs5{D#P zQ7tx?+!{Nj$0d_X!|eNYzTdy#ywCIbyuZDl=Xsy^d7sY%Hy1l836KN;0Fbi3V&ieh zf&YxS=;2;`iZKBI#NF&|th}%RYo#kBMVJEhgJa!-S~_Q13}Prmmfce<^nnzqt7YD= zW{M5@egcyD0K+r=HUwr?6I^!G_KzC;9G^(sZl4F%F0C5;xzlT~`Elj5438E2F(6`M zKI}SjV&0(bdg%Y)d>_E`y!(WX@}6d8{Tl*VO;ohyY*P)W?ri5M4nTXbwf*^8HZ2RCQ>REP3PRl2g zz$9>ZL&xp}_MP{OxVfLL4s?Tnb4{Y4+K8rCJ-sCxHr}?z%1B|*wb`WT$T4h;($%j& z^a2d{(q4VKvDw)1E`K&5{Awm#2`r*Di>C%6A}Y5^cB_fkhB*5F`& z$@0_Jv_VL5G;(CzPFI+)geXJlOSQNpXFDp6h_ZVwmO@Ny*S?$==zBXBXzh1dX8jbG zu^*~3FH2+M8q(}w9nQgHxy33k(L6`hD#BC%XA6|Jrk zsV6Awew>C{QDSV~iZcN$pXBsHUoRK)n*j30&5i0iP_|V2T^1D8c<;4F%{FDEJxw?K zoaWc;>2bNF&2LB83Q6j(p$lLktUTw?OSU#?CqekjFmq3HpV=fo&?y(3{FQz@Eiy=%WRniOUSOh9=?Y2!|CAI z=Nfe55nQ-~kmh6a&9nI3@Q#@hX|&zDV8O)_1_Q;&V$nomgatJp_dXN`$Bw)Do)}-D zg&55U;h&TUFS*V{M>RNlNSzx6FjB09a+qR!l)8kcp#15eH5 z%59XPJk_y@R>|>0=p3RG9EW*jmp>}Dn9A@nBGL5+HjS67&bXk=LtB5!h$N{#w;*1T zLgmF*9xb==1pCKXu@9wC0FUR?42JH+Q| zFX$nVK3PYHk=MUs$L$c24D4`*j^eemkn${(=SHDv>n1}x{xhj{_eBn+KNWb{Xe(jt zqpqdJRFDWA_RBcUEmb9;qn$x0ehdDv2$>#DtePeJ*2EPa;e)(%$3|!Kx_|G&=AT#A zCpR_PORsDPr}f8I9=Z?1Ph@cK)U=`G<$1@4H3X2(gQvF&derO(kf=|9ERGzq5`DJh z1By%S-iycrXBQ>TJ#~xnw17%kK4#Inf)5s~4kOd%dk@V&63N@&N`!@=n}rWEW2pfZ z(raZQPmW&9(iCX4! zfhLX~YHQaTg42Ac>U#dNA}=<2^)D@*mdlAyA^tf%|AEYcJ%aR;3lHAZ)k>7_KUhPg z3x%-bG^Ki48Lf(NO$N+LIbr-qDeT=cAAiCyuQ$DJ+kL0rKP=7I zp-5oUl(N>N{S7F%?KUOEJfAxIRMt|%f%}r4NLeFkPvpGVX}#^SNIo_|6U41vN%zEd zGM_z&hjNJtCZXfpV;{$;j9(<}Jm>q(lHy2#wY6eNCHw|VB|0y0PHn(O*~ zt^m-#Njhg?TDcPpOq&1QaZ)-6({6RxMI4KMhfZr9l?|3vSXv6De%TZ({r*p;-DDaa zU4OJILpL{jYbb`+@*?Je-F%iw70+tuV{myub76e3=)-1<(jcS;alpGtNNpaN&jZ`Dy zl;1pFRzaLzkt4481m*8FYH^KDF@Uod>o-NEg9#00ND;7nz4UtsS7C#rNbELOeBEZc z+JD>b);C+OK?<#(d~^h7(gI6?Eu@+$CF7hp>uouk!hTx7Prp99)Uz`(xd%+?-dVDC;QicoHWRM{&t&)pq%+th7#c#3r%b1Vh@F!Yoq!0 z#fDQ+R39$wxwI6>L)kk>=}C?Aewbm{GeIf7WqT5Qa7+~OuL%ARklVVPmo&yyYfSaw Pivh5=b+Ku>6qxp3lln;I literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e5adb7dca244683378d1a6516a585c3a7ac0e13c GIT binary patch literal 1657 zcmai#eK-?}9>?`2Z0ymPlg_BsrP!8(qe|*2s_pjgceDC^tyJ~A0YN@HI zX}euU`|sV*|42i9k27qVt(uy;gB#irn{;itG@$B~^AFjm1TipNv>~n)KZYflvK>0} za$zvD+rrR;yO}kgT$IWQie5#NJW>&WsE%XCh*;JYN-Hy{?fW}#?{r*@ z4m(8Kid5ZF?NixR!IfwgRRvWU1I#Ib>tnG^jrfFtivx=*;Fa@`D_3r|#Tz%8fKu{{ zz!m>YuX>DF?jJ#N$j)1408FFo2C?kPqkEkvI06|ab zihCqj(CYUaVJ9$3f!&<_>Ll3R-=vKy56O z)|;^3pRZhB5uEDpBaWo@wBgwWzxcC`x!I{lZ8UwA6Qhx~thx|nV#o^%R8_?VvsNFG zZ0P48t;CQbl5dXgeFN!xOJeKxOl~G6ZOOODqMhJX2NQ|Ejf+!}MJJ-~ZyYqibH)fp zLd$YkT1z>e{hnZd-U9>7Yr{l{T;&OoMEA_NTasdj%H~*BL>ya#*GY|{x~Hz|r)i!U z94kqK0@V4*=_Z|LB8WVhWq}=H0aX#bP`c~dt+wF zC~RM_)euYBkSt<+tco{Bp*|pFwtE){V@4mFtl`}hCG!D+*mik8Y zQ+DfWOUZbRC2booqWXA#^4yDD+}x&;>#^Zfkp4~#tcNa}4H@1P?xh}rw)XEJwmy)F zpi0r$qUSwTN-?sgU!&yORu>tBs3|pC4a7~p1WI3Y^j~kG1X9m>>VF0jlP8R1udI!Q zh#7d7kAC>otCKGS7jw2QHA%+Xv9CtN>(q0(CJRe9s0_q!vO-p4m7ld^K&ACQ47-q+ z++^qsPGsuW3B=*e|Fu3E)J~q}JgREYyc(%n%^ZEI`f@o-V4Q;g3B)lb?kisv% zi_62E&gjs^@N#_7@E_jvFa-W+3nV1pcHQa4K-O%~~8PAya9Kl}3Ggi+52W(bwDc#w8!CHNmGN!lqpD71_ zZOo5y8ILceAD~>X?EW3!ZXRNNa!Jo7^0f_N`uN{;Z05WAe-R3A0qK@k9RD>>1N07m z1{{k@K(4hyrG=7(u694gCRLoBBDiucFe|fc8WNKl`Wqo1LcVpYlXjTjAms+fT^&5q5>6!V$i=Bu zj4cVC6P}Y5J-_}J#Dhzc?(}vDuhm31jo$2@Fj>sd!d51=@I;ER+@7M>@G=n)$US-mV z99u|d55wyOy3ZW~xQwlsG&M;!RunydYg$3=>zxZolySn46Hc;{OFL4qv>Wi{eERIJ zC_4tl<_!;cFm-6wO1J}DwRmmvdZA21IF_bt> zXUV)N(^)}l{4?WAvWY8y7UD1XNyGQY+;M_)Rm*DGN%!$(4=P5@@+ktPvY})$GHC2=#Zl6Q{}H)xs#3gdED9RvDhBZ&0o*y3-qhw wwMQTDi(5D;+Rx$|9vuC@@E2gsn0{-hCm*Fh;~cD~_9jKm&Dk4W;}n|lZ~m=1vH$=8 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d606d4445f31807a785e65ce234a2de9b925582c GIT binary patch literal 1831 zcmb7_X*3&%0);bucBQtUnOefskg7~<(Mcs*VyQjhky?wigb-BhTUz71X%%gyC}MPk zv86FIRFeuOG}BbaSYv7_qKqY|N>H87JLkRsbI-Z=`*ZK_`yU@K7ZoLt5&!^DadUOT zd}jFHR*?IgZ_88d006lSH>V4JX`$;?)6M?=T0HL=OuxI_4(feXR#A5N?t1rLRVFQ^BDPw}J+Wvv7#R7I zJs!OCMAUcD_rLO%nWM!-OY17@fo=Z?r-vZd3z)yBji(9HuxHK#etm3CjPAMkQQNcB zf6|_(zCc_c*HW%6QMagD6egMx&>;@1XIQH0Zl27Qg)lAN9fT~HMV9fHYf)v7XY8(I zec9MTmkQIS0vU-aq15+;h0_bpwGl#5X4*a0`5FcH>x?a2tgnsWEnma=;Plx^dir||b;y`koc|SeJ|un2F80(-_?79~MdDP& z`R7vj(JXkODv=n~e1ws5CmW>iFHw8%@iy}pBVLOrTV06{_C0|_7o^3Vtksl=L+8xh zX=U!_IBv7|p*ViUd6(r^G3?26lT^1(RLdo&UH*taE2&1gFMWzT1s`u>JjG80%6}@& zRUCjRinh!5`53WnE_JZlQ@~zrh?%lsfgT*&@}LNOXEtS&Nr;iKE{}JQ_oIWv=ZtuD zTLs}eo&MQT;4~Dfv-w3#-ocnlKzkthiwi3GU@WeG7PO8~&oaDk$?CuQbq^v;Hdh04^%@HH@22cO zDZ>N;NhvI8szVCn&MKNgMezuAajA*CH>iAsSevYXM7O4!W??`EJsw(F4v#LF>5R5P zDNsBC$%l2uqQ&@uz#ysSx4KbdrzHo5wh>24A?Anu6C@kmr7q#Fr&ww%aGzq3i^`W2no}DpH z;*ZpfElAu6dDhg6Bw?N;Goes8W+9;mut{aj1S>6Eyn)PiYmx(zpp&)C4DI6MI!}H- zm-(IbETmzSP6l((hqc1vXzKL51IM3wWTa|WUB+HbHf{eyUv&|oxti=j_(r~e(pOt8 zE8cadT5&k0oUlM*VSDSr&-Ius;vDp5V8y7`* z4QL|y=G+tR`nv|*Pe=gibBYk-`Zaz%9DY)|O-UH=$Zq`=px0aPH-{#1a!m?h0uCN5 z!f722>8LoZd=!}v!11LY5PD)kH*>S8i)nLEY-ys1+}nvYWI)2|ZV9@z4xj0%xOV?Y z&My7s$ji`RsZrQzl6Ek(L$)?uO79eeXz$$0KdmQNGp!QKQvbwEH&!|fPg(Wo;sCH9=;rhMat(WYJ z3!x(8)kj2bCc+FF+&A2|ee~}2u|MEV!74c#Rrt7UumN)D(#kTluSy?-!YSo=1oGX} z)$f!`Z6X$Cfntz&rC?HlrIeiRmyJKGEUECFt9)r~xAF^rs^y3Z*}pkV;F*kT<&Z`W zF_D@Sl;Y3UF1JV-4?Hzd%EHemeU3n)#a1;mt5?SBb{)EFUL!ig z?eVF9y3XQ()OOGYIW#ugD}N!b~Lnn9M9A;zG22ni+e zl^@vvB#)C z7MOvHsmmEi;*k^)p(d=YR3jC2Dn&$2dZvbk${>_zo`phlPQM+RjR}n<@tryEnwQ?W zw?q*mcA+J!0sSA1uFm)CsaPYSLSRs`<^KvLsq<+yShsLIb=)#mH4(&VA5h6s7QdrN z8{;%0NhduB9*&G&R1N`J+}=uWBD)2Qc#DLaheMaaU(41!SLJ=@bn14qk8W=uY2dF% z0LASEYR2t=#G_Tb`PFX3pHrM-O8f83rcUGIpFg#hCu!{MWUCDs&M04XtB*5%r8 zv3S^COGV(lG84-nc(BxfWpI!hC94ZfwQ&CiCFTTJ87iUNSWgb_*^a>?yCUlDM-xo3 zrgf0UOw%6>}&Uz42}o)YS)xLt}c&%JRF%& zUa;|T62;bxov6PXnfv0A>DxyxP8yS8W(w0m+2QdBe#3!K;* z9ihhWt7r|x^<`E8St@hzC2O(i;(`>Ve6$b&IBR%L3%}-0tr*mdK$Yxo#O-hjE9A%1 zBqBw;%&?Xqeyz+dD$zck7A>_NbQg||vT8RDP7E}~3W0MpQ$KY{P+7DC(1q0%xzU^{ zmt#_k0Y!R0AXcj63n8hw<`T0T4IO%XNYj`r=8S|a1J+}PCG2xbj)>Q0D|H~W9j+t+ zqg8Od_j;MJlpNtUCh#)sN=FM)V~~*JtZH1;q?{(jY8HEc8mPX*3^jxkB7D!cC?%<5spvAEGVD zU^$-wWw*EN;^K9-%Hya?eyA_8pdSw>&Vdsp5-%T|(5A$mZBL4VgdbcM_C0{h-NfAf z=@en^c@+)&@*$aR-q$54?|~b>8LbhXRZg4CuDCxR{7q)!)pGuuv+0*Uk?$MBWjp%( z^52r6?KZb&k#+Tg*_+Rfw8Y7Um-#w3yc$;ZHLTt3JP&oL|<>KDW!7>RxPk2MhbM^-p}UX5Zg-PPb5E4&W-&_ymgPQ+ zGUyb+@$teZ)(>f9EeOH%8&7Onf9z7hDmbFDZA+hBv)vo2}A{zqctR-yk`Bxku9$!lt{| z9hI_A@dUPVioOS10eLfRbL37={Z?!8dcCqFS%XO4#+!Wp-l!JBZkDz@-0%6^h{{1e zrN$fvV;p?s1F8iSA#Dxlwivt#jxsBbQp50e7)xH$Ij9=4Wv#9}{HyG^L_{d%20QD4_^{pj?sm^SF%c`^zj@8JeN zk>*(maI!1YJGFOe67)v@;kLDGrmxPPAzg1qF)fQRea(2qqSD)QDqbu8jV zd6Z0~y!beyP9bSs4fNfS^Q-yclp9Vhr{V&iIL;!St;3#;rn>I8u~SYCDyoNUGxaU~Y1MVHt)e8*se3c@8yLf zRByB$CoGXcXDD*2YnX!Q}^Mt^P?at&U@gw z7i9%(d&HmVwiI}`b%OXh^QZB8$xwJqQ+|`zlVNe}$ki~|Q-c8S=TbZkHBa z_+{|uG}W9}uXN5JFb<@du-)8m$awlqsBLm}wY3)UESD>) zY~iLiV7G%M+)*3im!l5+Zhej>7~>g2FlP;%OBlH_0_ z0L!%IR&!;_NQJP=I^t|u`v?#Vb~G`KzWM1@B;wyUCk!f1UR;1Z2;7&+m04tb^G_9n zViQ6tZCDteC@LLn0nVvHd&5i_&ZK~w+eQ^nxgnRnXV!IA#b_2J2v{g?D2N}c@>K;I zndB&!%T9hx;Fqj7_2Zga#3W;rk>S_pd*FVZnh>%smQp8zl0Vm& zNEMxN3Os!who$$=!xej$w9S(qCr%OdyPS1Vo|)idUCHRy>V{;Ybe5B!8b*bpt!Ct< z@-LlG7hee`M}z)Pz=%8tw3`)0r80XimFV=!mJrm`$u^BGcPwf2+T$UHd*}EJ(X72})Cy z-Ij>~CE{k{xPseP$ZHPUrUI>Nx$)WAH0JYm;e9( literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_link_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_link_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2f99e5f5b80cfeabc6939a5e6e0eb6e401133d8c GIT binary patch literal 786 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z;x5o#WAGf*4vwB{em4u4t(U& zWeaCp)ETic(Yr~_so-Jsj01;$@U`e=s1~+9I@HUu#_h$j(`=KqJZt;fS~)u}F?B8x zc2q82eQ?8&uP*uwo^D*k{K!!`*XCXW^_doPBX zvU=`!4gBlwz70@1Z|xlV+ck-LUOb00D~e}z9Amjf@T@xZn;%Vbr`W1?u_@nU zSihw|{f^hN%K5>nlf>f~YGpLeK3k)DT2ghARQQ>O_FE@}x3fuHuv@HY{9xv#lzXq5 zTx;1SjUOz&bj0LcyYAa+f6Z|5vkkea*LovakF3}h6KlvEwS2>*QisTy=Xx5hSt{hq z1_VsA{BOVDx&6Yt-<{cY^^bg?vU#plYl|@Tk_;=FG%vf2Qy-%l;}A-`da4xAR?kbn3O#>lV(hf|})(SMw@8 zf4cF0@7g)9uLLjkX)FZ?Z2W>Z8-y=@`<%(&vH7ZLbhqlS=A3tYZAvce8mzbP*w%Ar zUt`pM=YRh;<8hwS>;~uU@z?JQUKUu*clcH+v(%nKIg4i5PrV;Dos|$YU2tmBbP0 Hl+XkKcxqY5 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/marker.png b/src/main/res/drawable-xxxhdpi/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..61ba6705576979031133e2c00c22f7386c9648af GIT binary patch literal 5177 zcmb7Ihd*22`@gaG-m_G#s-haRS|vplwQ3W4m#S5bqKI9q)U5SECAIgC5wk|Bh}x8v z7>QM*<(K~cgYSLad(L^?bMAdT=XpQl{hYX``daifJTw3R(CcVx8WDTczlVx~*cvT( zQ4%|{mk)JJsE8Mg$|)8ASn)cV>L$T?yNm9=0>`10`2DbC*Wazj3vG;6P5Se7WCtLI zfdMREx}TDSs6-UUcTv7w{{i>%^oSEvvCwq*))Tu1`WxADr4JYiNuzulK#9~%U+f)W z!rN1s7k~cF#_%RESwpvoT-zH#T(EtXMXmeJJ(e;SSJvU~z@>)2otjEO+b zvxR+EI^PNwWzR}1cBg+pR3%51<_`lksGQD}V~d9)LVtegEIgyY%QoQ*!&O@8D-w6aV28KaA zCR$NVRd8^4m`$#g+gK%7{Q`L^a#Z5HVO4DO=-iEbpu-J3lVNv@Gg*wzf^me5Fq%({|-)-UzQ;(I{ zrmu%^bg<||yS8qf1=(jB^V*WWr1o=CbJn`AcWp`X^K@g&{Jzs_l@WY=lGpu3qW!B1 zbHy$MopT!_Jvx##ZIk9PY9VE-+MY8_UF|4(Z$?rd)2>}A_J0>aiyFj zq#lbLutNmU)hQ9%##8FCPZ}2l3eHY+X$X7eNoUY~KQ3GIUC;|jL?Ni8p7uDu)k(nd zrF-SLO}@~l5#Y&S-S_2ge}>yI#b`~q!R;!6FQR^@6OpW0dQQx7&hp)TMgc;f+WvoC z3He?>c~~Yf@xSWd}WRnV^+=qBoRR%Np~;l%6pezE!xhydwmSXSNCmI z6Y#LI#~u%95u5ZfY6Y0fvn0a~U#FZPLw{T}WBF>6j;X2Gr+DBAy6(Hd68(Cs2J?S) z(-&i5O2b~DtH0`G(x(0Xs>oNnn^$~tp}^O zWn}YieNbk$G7$Z?jGyn_8WEDj3FlzWL79P{a={0SGT%ugX>!W4UtyyvZuW4xOr6dy zzgu6&kUd%Iu`(j3S5(eKrTfIlDtqox#<(v77!&;q0*({^Rm)_{CJv_FSFM)!5NW4Q zw2v>!@!uMJ!ix@H5D^vGci>t|--pY}v#i@B*2y{po9RlsTGciv>omzubdJF$p`X{0 z=)q6UnB7N!KMrm*l={Z5W)&STJp{VBa@L3+$=CpDv|HBuRvsDjdXG-^76D-&-#pqa@@Y>xs&+9RR6(wJrg|Z z6nHPr^+3@#M4%vg?2tnS0{ET14fIunv$Afs%7g@{=JmgJEgs7LaSIy%aYb7yp(r)0 zYW^>~&Vzb0b>cmFqhvr*J(c6*{Vi)y{V<>CX1$_H{=ksGW%F@Xs}qCPBZt_}hX>M+ zb+Uv3F)?Ey`QXD#qIh z=J&uJ+ZFidQ1B@U4oakVc+(4pJBe1`3xE>Cry22Wj^KT1B-v-bJS~t%mi7j!Vw{$|IRC8zXm%eNY z$2?behR8reTP`)3!qhEYeMx`Yc`+8AH_v}l)8XfC{BzA21uUV@Y+`+E{ruNYMO79p zk<1O7#A5C?cJbWXZa>S6xu$xYI3X;)hsuls?Tw`(3Rmd1ZLi>+8JC5+@e9aCb~$EF z{*lX9_&)1Om}o-E2#1V3%M`k$0H?Bm4Rd6*ggu^nhuJnJ{9R1KF0D$>%T}4FlfuW* z{sgcje3jHUSY*7N%Z<^pHY~`|!simAXG?zJ)vQ881rZN~5qd|AGgd`sewe zpJtG&`iF*EsJ)ghMUKO@0_7iMoY{663>vq2v)kywa(w zucv5#t;&9X@59)Q`m9)azw%$a|W9xm4DR5v)?}v){ERb zx62%z1SzVGH!Jv=HB;sb;p*nkbk(5`T!-RqY+%bI9o6c!bo{_aRy#Hw6cow&Cgw{e z-J6(1y-NM6vEn0&6>+|e*~{e}(;CS_Tfbn98`{DeNIOW$U~eVe`k@Ksis<-@oiL(# z4h`>bb&LNp~(ig+BIaZ)e>-J&gpx{v5%M)uLFU~2Vu>OanyB*`@>!+%Zv;>$L4 zFgsh5!Ty~$LA6?4f=d}t&Al4eFAtDql2R=|x4OUStQF$C>Pm4SP6a%FbFc9FlzU#z z6QRLcfs`>%MSdN7C%9d*;)Lf@8aTs)Q2Orlf^l{{F>TMjsx_{aV)z&*-1p)c2YGnO zNnr+#V5b6(H0aZjHkLair`H-}3Ess%*`Qax0U!u47Zvy!1MXhn1-Wp!PVj~lSP45sKl^KV)k{5w`&AABsnT;9Db z^pc;!mMBd)#KDd6AfN9eQJ{X_5=C(4C#>v!Zrzo^>SVuy-78pgk{pk ztabR;2fq-x=&;}oRedw)+i|=J&wbcNhjd`iR#p9$LsGE=SKCp%2vnmaKS&k;d_uu7 zVX$MEg{yXc+<*8!vq$(D*O$&J1Ad=<8jOM9I&Bh90;27dVeAJ@@Yy=I>Sb?h9gHDV zE)%tW`(xh~Otr6Z?q%w-Pzf5@+gs0GOm;f)OFV5tsHl9YETP;%*$jo$a`p9-i;JnQ z9vmO%3HD;?XJ5L0_7+Y=teQW!ht(%>YpveZ(4doEEoRk6aGet6nO@t~^X}@NYWB%n_Bmr#zoFT4E>lVU{V*`Gvn!gsWQYN``@pe7G(Ce$SQ*4l^070k5`*{Y0r)aafsWs z&Wx0zG$wn%#)Ye_FG7nL+_>jSkTjo##x#5_yOl2s0RX5?B!7Nm6T5;^+9)eIeB6>Z zWc%!Q*N1p_5@=(rmvJuZ8`cIrT*k7HJ#h?9V3>)NV4flafTquZMYWG22=k&xF|viV zk9snv^_L(L62|0(!zC+-XJS&t6_-*rHL)aB9s$L}j#VL<6?<%k(49@&+j$gr6%UfF z5wTV?uddN-BAl`%0idP}1E<&9toUyWVwcX3<2jN&XnOyI6ENkN>q2UDB-kqlF;Wz6 z+sl4qxnn~VJSdscQ_&zxA^qgqRb|!)`u#tJcXcXhhmzd5Mnm`eIYUm_I?kbt4yeI% zxXt#se0?-4@U#{=pJxz;>IYFNIuYZeuMHUeOEbcL|3Ns8<^6`scR)NXyt?v5kuhcZ zW)gxJg}ptk;RV0j*`_X?-=fTscIb}+jML=Lh&l}ndOf*mCX@wTxGK#81E}6h3oEnM zNq7A8Xz;JkjcYm6(paUvo~P7h4B;rQ`AMh2DrrV3(KksSa-`&Tc0TvOAGKE$T!s&m z0suH+sHyUWqj&5Qi5a4P2ENOGw?<+UciNV8mzIw>8W&7T08~ zYGMkIMbw5yf)+oESBl|ycCZRuQ78m5)5H`606J{g5*6Mj%RTjcfeEqQ9YsA2*VR54 z1%QC?TgbeYqIP(YtiPkH89lf;+q3Q>12F<9vN4ARn*w7EDI7VSG<#jjPa6+EA?1KH z@tQ7C6=K=Z6IK7GDoaWZ78=PIs<&ye<=u*q1ArBy8^p~NchB{-i!U50sQA9D=9F#> z;bcXA2dGFKJCDH&ZA+dx(Y7D@yfb`9m2Zb^dzT5~o8BYn0bnQ%^?d(u{rp`HB1cJ- znodP|v88gt_4Hm>yy-M?9|amNLB|)9mN(X;xJWJkN}e!~G5J~|R&Lu`P7|x$rl$7V z_Cd$(im(J?T1>LJ+v3ugK;+Xsf6V^Sh^L|Uxappuuhr}+^X|>DA&Oh3BGg)eBZlz5E1@Z;p*p?*NtY-*D<_AZWPQ};$40e*+_@ zK|!1hp^@JD3v>GlXc=k?rxxULG7$n3!e1J;@amPIp_Y>6e8&#b8=a2yT2@ zR6?BeLFVxU0dMvuuoIeec814es75yfUq0`FG1Wq@+Axr>$cY$2gN`V%g5C_hRNmqw zq$l(gzFJ}7rZ|xg8-;*%QB2@BM~#w8v56zFB7W!{#S&VEjt0f!KC`3MI)1m!kIa0$1k8 zix5VOD(VDqGsm_I?a`TcCD=nq-)m1g_Y{OyR()ghb5|}QVgAEjcP%rHm!GMk9_b_w+z_Ejw3*-QC7t(R_I>}Hp=v^UUBybH zrEC$R5Ef_1U)cYdBNbB2?Hw!`UCyX4oGD$E{-r3E=$bL}=dc>gw}G@0;C;lhyM4K6 z+V`_;d0Q25`>d}qiZ`g!2fv&% z1SC;avHxJS$5}>5If^@BpM#Qv~c^<<`l*}PuVnGbls`cJ&9ct^y!)* z6V~*W{-3je03uI-df8`bBp9HBnrH26GRp2g;|V1(&4RF)%cak*ZBeM6^jf$90&|AR z{QP&Hy1AI44FSa(OU=yj%|>l0T7al(ttlSr|K<3Mc-Cf8aYeaab}a9eH5X4j3j}l^ L`kGY_p>O{WNQA63 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable/white_cursor.xml b/src/main/res/drawable/white_cursor.xml index d89e81235..f7b4d4941 100644 --- a/src/main/res/drawable/white_cursor.xml +++ b/src/main/res/drawable/white_cursor.xml @@ -28,5 +28,5 @@ --> - + \ No newline at end of file diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml index 6c9c93955..33890e30d 100644 --- a/src/main/res/layout/account_row.xml +++ b/src/main/res/layout/account_row.xml @@ -21,7 +21,7 @@ app:riv_corner_radius="2dp" />