From 64bdd7e731c11407bfe6b8b1ec6a254ffb989057 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Wed, 22 Oct 2014 12:38:44 -0400 Subject: [PATCH] Use Gradle build system --- .gitignore | 41 + .gitmodules | 9 + build.gradle | 16 + conversations/build.gradle | 28 + conversations/src/main/AndroidManifest.xml | 124 ++ .../java/eu/siacs/conversations/Config.java | 25 + .../siacs/conversations/crypto/OtrEngine.java | 231 ++ .../siacs/conversations/crypto/PgpEngine.java | 385 ++++ .../entities/AbstractEntity.java | 21 + .../siacs/conversations/entities/Account.java | 399 ++++ .../conversations/entities/Bookmark.java | 137 ++ .../siacs/conversations/entities/Contact.java | 367 ++++ .../conversations/entities/Conversation.java | 500 +++++ .../conversations/entities/Downloadable.java | 21 + .../entities/DownloadableFile.java | 154 ++ .../conversations/entities/ListItem.java | 7 + .../siacs/conversations/entities/Message.java | 478 ++++ .../conversations/entities/MucOptions.java | 369 ++++ .../conversations/entities/Presences.java | 76 + .../siacs/conversations/entities/Roster.java | 83 + .../generator/AbstractGenerator.java | 48 + .../conversations/generator/IqGenerator.java | 96 + .../generator/MessageGenerator.java | 178 ++ .../generator/PresenceGenerator.java | 57 + .../conversations/http/HttpConnection.java | 255 +++ .../http/HttpConnectionManager.java | 28 + .../conversations/parser/AbstractParser.java | 92 + .../siacs/conversations/parser/IqParser.java | 92 + .../conversations/parser/MessageParser.java | 517 +++++ .../conversations/parser/PresenceParser.java | 133 ++ .../persistance/DatabaseBackend.java | 335 +++ .../persistance/FileBackend.java | 480 ++++ .../persistance/OnPhoneContactsMerged.java | 5 + .../services/AbstractConnectionManager.java | 23 + .../conversations/services/AvatarService.java | 298 +++ .../conversations/services/EventReceiver.java | 24 + .../services/NotificationService.java | 237 ++ .../services/XmppConnectionService.java | 1927 +++++++++++++++++ .../ui/ChooseContactActivity.java | 145 ++ .../ui/ConferenceDetailsActivity.java | 280 +++ .../ui/ContactDetailsActivity.java | 436 ++++ .../ui/ConversationActivity.java | 947 ++++++++ .../ui/ConversationFragment.java | 781 +++++++ .../conversations/ui/EditAccountActivity.java | 423 ++++ .../siacs/conversations/ui/EditMessage.java | 39 + .../ui/ManageAccountActivity.java | 217 ++ .../ui/PublishProfilePictureActivity.java | 242 +++ .../conversations/ui/SettingsActivity.java | 74 + .../conversations/ui/SettingsFragment.java | 15 + .../conversations/ui/ShareWithActivity.java | 185 ++ .../ui/StartConversationActivity.java | 677 ++++++ .../eu/siacs/conversations/ui/UiCallback.java | 11 + .../siacs/conversations/ui/XmppActivity.java | 637 ++++++ .../ui/adapter/AccountAdapter.java | 102 + .../ui/adapter/ConversationAdapter.java | 135 ++ .../ui/adapter/KnownHostsAdapter.java | 74 + .../ui/adapter/ListItemAdapter.java | 44 + .../ui/adapter/MessageAdapter.java | 560 +++++ .../conversations/utils/CryptoHelper.java | 112 + .../siacs/conversations/utils/DNSHelper.java | 185 ++ .../conversations/utils/ExceptionHandler.java | 44 + .../conversations/utils/ExceptionHelper.java | 117 + .../utils/OnPhoneContactsLoadedListener.java | 9 + .../siacs/conversations/utils/PRNGFixes.java | 327 +++ .../conversations/utils/PhoneHelper.java | 95 + .../siacs/conversations/utils/UIHelper.java | 225 ++ .../siacs/conversations/utils/Validator.java | 14 + .../siacs/conversations/utils/XmlHelper.java | 12 + .../utils/zlib/ZLibInputStream.java | 54 + .../utils/zlib/ZLibOutputStream.java | 95 + .../eu/siacs/conversations/xml/Element.java | 148 ++ .../java/eu/siacs/conversations/xml/Tag.java | 104 + .../eu/siacs/conversations/xml/TagWriter.java | 114 + .../eu/siacs/conversations/xml/XmlReader.java | 141 ++ .../conversations/xmpp/OnBindListener.java | 7 + .../xmpp/OnContactStatusChanged.java | 7 + .../xmpp/OnIqPacketReceived.java | 8 + .../xmpp/OnMessageAcknowledged.java | 7 + .../xmpp/OnMessagePacketReceived.java | 8 + .../xmpp/OnPresencePacketReceived.java | 8 + .../conversations/xmpp/OnStatusChanged.java | 7 + .../conversations/xmpp/PacketReceived.java | 5 + .../conversations/xmpp/XmppConnection.java | 1130 ++++++++++ .../xmpp/jingle/JingleCandidate.java | 143 ++ .../xmpp/jingle/JingleConnection.java | 910 ++++++++ .../xmpp/jingle/JingleConnectionManager.java | 163 ++ .../xmpp/jingle/JingleInbandTransport.java | 191 ++ .../xmpp/jingle/JingleSocks5Transport.java | 212 ++ .../xmpp/jingle/JingleTransport.java | 13 + .../OnFileTransmissionStatusChanged.java | 9 + .../xmpp/jingle/OnJinglePacketReceived.java | 9 + .../xmpp/jingle/OnPrimaryCandidateFound.java | 6 + .../xmpp/jingle/OnTransportConnected.java | 7 + .../xmpp/jingle/stanzas/Content.java | 102 + .../xmpp/jingle/stanzas/JinglePacket.java | 95 + .../xmpp/jingle/stanzas/Reason.java | 13 + .../siacs/conversations/xmpp/pep/Avatar.java | 71 + .../xmpp/stanzas/AbstractStanza.java | 34 + .../conversations/xmpp/stanzas/IqPacket.java | 76 + .../xmpp/stanzas/MessagePacket.java | 66 + .../xmpp/stanzas/PresencePacket.java | 8 + .../xmpp/stanzas/csi/ActivePacket.java | 10 + .../xmpp/stanzas/csi/InactivePacket.java | 10 + .../xmpp/stanzas/streammgmt/AckPacket.java | 13 + .../xmpp/stanzas/streammgmt/EnablePacket.java | 13 + .../stanzas/streammgmt/RequestPacket.java | 12 + .../xmpp/stanzas/streammgmt/ResumePacket.java | 14 + .../res/drawable-hdpi/ic_action_add_group.png | Bin 0 -> 876 bytes .../drawable-hdpi/ic_action_add_person.png | Bin 0 -> 616 bytes .../main/res/drawable-hdpi/ic_action_chat.png | Bin 0 -> 295 bytes .../main/res/drawable-hdpi/ic_action_copy.png | Bin 0 -> 381 bytes .../res/drawable-hdpi/ic_action_discard.png | Bin 0 -> 450 bytes .../main/res/drawable-hdpi/ic_action_edit.png | Bin 0 -> 765 bytes .../res/drawable-hdpi/ic_action_edit_dark.png | Bin 0 -> 884 bytes .../res/drawable-hdpi/ic_action_group.png | Bin 0 -> 776 bytes .../main/res/drawable-hdpi/ic_action_new.png | Bin 0 -> 262 bytes .../ic_action_new_attachment.png | Bin 0 -> 587 bytes .../drawable-hdpi/ic_action_not_secure.png | Bin 0 -> 367 bytes .../res/drawable-hdpi/ic_action_refresh.png | Bin 0 -> 678 bytes .../res/drawable-hdpi/ic_action_remove.png | Bin 0 -> 448 bytes .../res/drawable-hdpi/ic_action_search.png | Bin 0 -> 650 bytes .../res/drawable-hdpi/ic_action_secure.png | Bin 0 -> 384 bytes .../drawable-hdpi/ic_action_send_now_away.png | Bin 0 -> 932 bytes .../drawable-hdpi/ic_action_send_now_dnd.png | Bin 0 -> 1135 bytes .../ic_action_send_now_offline.png | Bin 0 -> 767 bytes .../ic_action_send_now_online.png | Bin 0 -> 1095 bytes .../main/res/drawable-hdpi/ic_activity.png | Bin 0 -> 3040 bytes .../main/res/drawable-hdpi/ic_indicator.png | Bin 0 -> 684 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4416 bytes .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 1033 bytes .../src/main/res/drawable-hdpi/ic_profile.png | Bin 0 -> 999 bytes .../drawable-hdpi/ic_received_indicator.png | Bin 0 -> 686 bytes .../res/drawable-hdpi/ic_secure_indicator.png | Bin 0 -> 294 bytes .../tab_selected_conversations.9.png | Bin 0 -> 99 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 99 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 105 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 101 bytes ...tab_unselected_focused_conversations.9.png | Bin 0 -> 93 bytes ...tab_unselected_pressed_conversations.9.png | Bin 0 -> 100 bytes .../res/drawable-mdpi/ic_action_add_group.png | Bin 0 -> 634 bytes .../drawable-mdpi/ic_action_add_person.png | Bin 0 -> 469 bytes .../main/res/drawable-mdpi/ic_action_chat.png | Bin 0 -> 261 bytes .../main/res/drawable-mdpi/ic_action_copy.png | Bin 0 -> 288 bytes .../res/drawable-mdpi/ic_action_discard.png | Bin 0 -> 324 bytes .../main/res/drawable-mdpi/ic_action_edit.png | Bin 0 -> 522 bytes .../res/drawable-mdpi/ic_action_edit_dark.png | Bin 0 -> 587 bytes .../res/drawable-mdpi/ic_action_group.png | Bin 0 -> 546 bytes .../main/res/drawable-mdpi/ic_action_new.png | Bin 0 -> 185 bytes .../ic_action_new_attachment.png | Bin 0 -> 415 bytes .../drawable-mdpi/ic_action_not_secure.png | Bin 0 -> 298 bytes .../res/drawable-mdpi/ic_action_refresh.png | Bin 0 -> 507 bytes .../res/drawable-mdpi/ic_action_remove.png | Bin 0 -> 282 bytes .../res/drawable-mdpi/ic_action_search.png | Bin 0 -> 449 bytes .../res/drawable-mdpi/ic_action_secure.png | Bin 0 -> 304 bytes .../drawable-mdpi/ic_action_send_now_away.png | Bin 0 -> 650 bytes .../drawable-mdpi/ic_action_send_now_dnd.png | Bin 0 -> 784 bytes .../ic_action_send_now_offline.png | Bin 0 -> 535 bytes .../ic_action_send_now_online.png | Bin 0 -> 779 bytes .../main/res/drawable-mdpi/ic_activity.png | Bin 0 -> 1854 bytes .../main/res/drawable-mdpi/ic_indicator.png | Bin 0 -> 490 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2726 bytes .../res/drawable-mdpi/ic_notification.png | Bin 0 -> 681 bytes .../src/main/res/drawable-mdpi/ic_profile.png | Bin 0 -> 622 bytes .../drawable-mdpi/ic_received_indicator.png | Bin 0 -> 447 bytes .../res/drawable-mdpi/ic_secure_indicator.png | Bin 0 -> 295 bytes .../tab_selected_conversations.9.png | Bin 0 -> 96 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 96 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 102 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 105 bytes ...tab_unselected_focused_conversations.9.png | Bin 0 -> 90 bytes ...tab_unselected_pressed_conversations.9.png | Bin 0 -> 97 bytes .../drawable-xhdpi/ic_action_add_group.png | Bin 0 -> 1122 bytes .../drawable-xhdpi/ic_action_add_person.png | Bin 0 -> 798 bytes .../res/drawable-xhdpi/ic_action_chat.png | Bin 0 -> 310 bytes .../res/drawable-xhdpi/ic_action_copy.png | Bin 0 -> 353 bytes .../res/drawable-xhdpi/ic_action_discard.png | Bin 0 -> 543 bytes .../res/drawable-xhdpi/ic_action_edit.png | Bin 0 -> 994 bytes .../drawable-xhdpi/ic_action_edit_dark.png | Bin 0 -> 1179 bytes .../res/drawable-xhdpi/ic_action_group.png | Bin 0 -> 1048 bytes .../main/res/drawable-xhdpi/ic_action_new.png | Bin 0 -> 234 bytes .../ic_action_new_attachment.png | Bin 0 -> 753 bytes .../drawable-xhdpi/ic_action_not_secure.png | Bin 0 -> 482 bytes .../res/drawable-xhdpi/ic_action_refresh.png | Bin 0 -> 901 bytes .../res/drawable-xhdpi/ic_action_remove.png | Bin 0 -> 513 bytes .../res/drawable-xhdpi/ic_action_search.png | Bin 0 -> 827 bytes .../res/drawable-xhdpi/ic_action_secure.png | Bin 0 -> 468 bytes .../ic_action_send_now_away.png | Bin 0 -> 1180 bytes .../drawable-xhdpi/ic_action_send_now_dnd.png | Bin 0 -> 1438 bytes .../ic_action_send_now_offline.png | Bin 0 -> 968 bytes .../ic_action_send_now_online.png | Bin 0 -> 1395 bytes .../main/res/drawable-xhdpi/ic_activity.png | Bin 0 -> 4349 bytes .../main/res/drawable-xhdpi/ic_indicator.png | Bin 0 -> 915 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 6503 bytes .../res/drawable-xhdpi/ic_notification.png | Bin 0 -> 1407 bytes .../main/res/drawable-xhdpi/ic_profile.png | Bin 0 -> 1374 bytes .../drawable-xhdpi/ic_received_indicator.png | Bin 0 -> 855 bytes .../drawable-xhdpi/ic_secure_indicator.png | Bin 0 -> 410 bytes .../tab_selected_conversations.9.png | Bin 0 -> 104 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 103 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 110 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 112 bytes ...tab_unselected_focused_conversations.9.png | Bin 0 -> 93 bytes ...tab_unselected_pressed_conversations.9.png | Bin 0 -> 101 bytes .../drawable-xxhdpi/ic_action_add_group.png | Bin 0 -> 1643 bytes .../drawable-xxhdpi/ic_action_add_person.png | Bin 0 -> 1088 bytes .../res/drawable-xxhdpi/ic_action_chat.png | Bin 0 -> 383 bytes .../res/drawable-xxhdpi/ic_action_copy.png | Bin 0 -> 470 bytes .../res/drawable-xxhdpi/ic_action_discard.png | Bin 0 -> 765 bytes .../res/drawable-xxhdpi/ic_action_edit.png | Bin 0 -> 1458 bytes .../drawable-xxhdpi/ic_action_edit_dark.png | Bin 0 -> 1670 bytes .../res/drawable-xxhdpi/ic_action_group.png | Bin 0 -> 1475 bytes .../res/drawable-xxhdpi/ic_action_new.png | Bin 0 -> 288 bytes .../ic_action_new_attachment.png | Bin 0 -> 1048 bytes .../drawable-xxhdpi/ic_action_not_secure.png | Bin 0 -> 593 bytes .../res/drawable-xxhdpi/ic_action_refresh.png | Bin 0 -> 1274 bytes .../res/drawable-xxhdpi/ic_action_remove.png | Bin 0 -> 681 bytes .../res/drawable-xxhdpi/ic_action_search.png | Bin 0 -> 1152 bytes .../res/drawable-xxhdpi/ic_action_secure.png | Bin 0 -> 586 bytes .../ic_action_send_now_away.png | Bin 0 -> 1426 bytes .../ic_action_send_now_dnd.png | Bin 0 -> 1456 bytes .../ic_action_send_now_offline.png | Bin 0 -> 1433 bytes .../ic_action_send_now_online.png | Bin 0 -> 1458 bytes .../main/res/drawable-xxhdpi/ic_activity.png | Bin 0 -> 7209 bytes .../main/res/drawable-xxhdpi/ic_indicator.png | Bin 0 -> 1298 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 11054 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 2250 bytes .../main/res/drawable-xxhdpi/ic_profile.png | Bin 0 -> 2137 bytes .../drawable-xxhdpi/ic_received_indicator.png | Bin 0 -> 1236 bytes .../drawable-xxhdpi/ic_secure_indicator.png | Bin 0 -> 380 bytes .../tab_selected_conversations.9.png | Bin 0 -> 108 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 108 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 114 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 109 bytes ...tab_unselected_focused_conversations.9.png | Bin 0 -> 95 bytes ...tab_unselected_pressed_conversations.9.png | Bin 0 -> 102 bytes .../res/drawable/actionbar_tab_indicator.xml | 21 + .../res/drawable/es_slidingpane_shadow.xml | 12 + conversations/src/main/res/drawable/grey.xml | 7 + .../src/main/res/drawable/greybackground.xml | 6 + .../src/main/res/drawable/infocard_border.xml | 19 + .../src/main/res/drawable/message_border.xml | 15 + .../src/main/res/drawable/snackbar.xml | 14 + .../fragment_conversations_overview.xml | 30 + .../fragment_conversations_overview.xml | 30 + .../fragment_conversations_overview.xml | 30 + .../fragment_conversations_overview.xml | 32 + .../src/main/res/layout/account_row.xml | 43 + .../src/main/res/layout/actionview_search.xml | 19 + .../res/layout/activity_choose_contact.xml | 13 + .../res/layout/activity_contact_details.xml | 114 + .../main/res/layout/activity_edit_account.xml | 272 +++ .../main/res/layout/activity_muc_details.xml | 119 + .../activity_publish_profile_picture.xml | 106 + .../layout/activity_start_conversation.xml | 8 + conversations/src/main/res/layout/contact.xml | 51 + .../src/main/res/layout/contact_key.xml | 41 + .../main/res/layout/conversation_list_row.xml | 68 + .../main/res/layout/create_contact_dialog.xml | 39 + .../main/res/layout/dialog_clear_history.xml | 21 + .../src/main/res/layout/dialog_verify_otr.xml | 60 + .../main/res/layout/fragment_conversation.xml | 102 + .../fragment_conversations_overview.xml | 30 + .../res/layout/join_conference_dialog.xml | 47 + .../src/main/res/layout/manage_accounts.xml | 16 + .../src/main/res/layout/message_null.xml | 7 + .../src/main/res/layout/message_received.xml | 97 + .../src/main/res/layout/message_sent.xml | 108 + .../src/main/res/layout/message_status.xml | 22 + .../src/main/res/layout/quickedit.xml | 19 + .../src/main/res/layout/share_with.xml | 13 + .../src/main/res/menu/attachment_choices.xml | 15 + .../src/main/res/menu/choose_contact.xml | 11 + .../src/main/res/menu/conference_context.xml | 11 + .../src/main/res/menu/contact_context.xml | 14 + .../src/main/res/menu/contact_details.xml | 27 + .../src/main/res/menu/conversations.xml | 63 + .../src/main/res/menu/encryption_choices.xml | 16 + .../src/main/res/menu/manageaccounts.xml | 15 + .../main/res/menu/manageaccounts_context.xml | 21 + .../src/main/res/menu/muc_details.xml | 21 + .../src/main/res/menu/share_with.xml | 11 + .../src/main/res/menu/start_conversation.xml | 31 + .../src/main/res/values-ca/arrays.xml | 24 + .../src/main/res/values-ca/strings.xml | 83 + .../src/main/res/values-cs/arrays.xml | 39 + .../src/main/res/values-cs/strings.xml | 260 +++ .../src/main/res/values-de/arrays.xml | 31 + .../src/main/res/values-de/strings.xml | 269 +++ .../src/main/res/values-es/arrays.xml | 39 + .../src/main/res/values-es/strings.xml | 269 +++ .../src/main/res/values-eu/arrays.xml | 39 + .../src/main/res/values-eu/strings.xml | 276 +++ .../src/main/res/values-fr/arrays.xml | 24 + .../src/main/res/values-fr/strings.xml | 273 +++ .../src/main/res/values-gl/arrays.xml | 24 + .../src/main/res/values-gl/strings.xml | 130 ++ .../src/main/res/values-it/arrays.xml | 39 + .../src/main/res/values-it/strings.xml | 260 +++ .../src/main/res/values-iw/arrays.xml | 24 + .../src/main/res/values-iw/strings.xml | 224 ++ .../src/main/res/values-nl/arrays.xml | 24 + .../src/main/res/values-nl/strings.xml | 233 ++ .../src/main/res/values-ru/arrays.xml | 24 + .../src/main/res/values-ru/strings.xml | 260 +++ .../src/main/res/values-sv/arrays.xml | 24 + .../src/main/res/values-sv/strings.xml | 260 +++ .../src/main/res/values-zh-rCN/arrays.xml | 39 + .../src/main/res/values-zh-rCN/strings.xml | 260 +++ .../src/main/res/values-zh-rTW/arrays.xml | 39 + .../src/main/res/values-zh-rTW/strings.xml | 263 +++ conversations/src/main/res/values/arrays.xml | 39 + conversations/src/main/res/values/attrs.xml | 8 + conversations/src/main/res/values/colors.xml | 17 + conversations/src/main/res/values/strings.xml | 276 +++ conversations/src/main/res/values/styles.xml | 8 + conversations/src/main/res/values/themes.xml | 35 + .../src/main/res/xml/preferences.xml | 114 + gradlew | 164 ++ gradlew.bat | 90 + memorizingTrustManager | 1 + minidns | 1 + openpgpapilib | 1 + settings.gradle | 4 + 323 files changed, 25921 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 build.gradle create mode 100644 conversations/build.gradle create mode 100644 conversations/src/main/AndroidManifest.xml create mode 100644 conversations/src/main/java/eu/siacs/conversations/Config.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Account.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Contact.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Message.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Presences.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Roster.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/Validator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/Element.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/Tag.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable/actionbar_tab_indicator.xml create mode 100644 conversations/src/main/res/drawable/es_slidingpane_shadow.xml create mode 100644 conversations/src/main/res/drawable/grey.xml create mode 100644 conversations/src/main/res/drawable/greybackground.xml create mode 100644 conversations/src/main/res/drawable/infocard_border.xml create mode 100644 conversations/src/main/res/drawable/message_border.xml create mode 100644 conversations/src/main/res/drawable/snackbar.xml create mode 100644 conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout/account_row.xml create mode 100644 conversations/src/main/res/layout/actionview_search.xml create mode 100644 conversations/src/main/res/layout/activity_choose_contact.xml create mode 100644 conversations/src/main/res/layout/activity_contact_details.xml create mode 100644 conversations/src/main/res/layout/activity_edit_account.xml create mode 100644 conversations/src/main/res/layout/activity_muc_details.xml create mode 100644 conversations/src/main/res/layout/activity_publish_profile_picture.xml create mode 100644 conversations/src/main/res/layout/activity_start_conversation.xml create mode 100644 conversations/src/main/res/layout/contact.xml create mode 100644 conversations/src/main/res/layout/contact_key.xml create mode 100644 conversations/src/main/res/layout/conversation_list_row.xml create mode 100644 conversations/src/main/res/layout/create_contact_dialog.xml create mode 100644 conversations/src/main/res/layout/dialog_clear_history.xml create mode 100644 conversations/src/main/res/layout/dialog_verify_otr.xml create mode 100644 conversations/src/main/res/layout/fragment_conversation.xml create mode 100644 conversations/src/main/res/layout/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout/join_conference_dialog.xml create mode 100644 conversations/src/main/res/layout/manage_accounts.xml create mode 100644 conversations/src/main/res/layout/message_null.xml create mode 100644 conversations/src/main/res/layout/message_received.xml create mode 100644 conversations/src/main/res/layout/message_sent.xml create mode 100644 conversations/src/main/res/layout/message_status.xml create mode 100644 conversations/src/main/res/layout/quickedit.xml create mode 100644 conversations/src/main/res/layout/share_with.xml create mode 100644 conversations/src/main/res/menu/attachment_choices.xml create mode 100644 conversations/src/main/res/menu/choose_contact.xml create mode 100644 conversations/src/main/res/menu/conference_context.xml create mode 100644 conversations/src/main/res/menu/contact_context.xml create mode 100644 conversations/src/main/res/menu/contact_details.xml create mode 100644 conversations/src/main/res/menu/conversations.xml create mode 100644 conversations/src/main/res/menu/encryption_choices.xml create mode 100644 conversations/src/main/res/menu/manageaccounts.xml create mode 100644 conversations/src/main/res/menu/manageaccounts_context.xml create mode 100644 conversations/src/main/res/menu/muc_details.xml create mode 100644 conversations/src/main/res/menu/share_with.xml create mode 100644 conversations/src/main/res/menu/start_conversation.xml create mode 100644 conversations/src/main/res/values-ca/arrays.xml create mode 100644 conversations/src/main/res/values-ca/strings.xml create mode 100644 conversations/src/main/res/values-cs/arrays.xml create mode 100644 conversations/src/main/res/values-cs/strings.xml create mode 100644 conversations/src/main/res/values-de/arrays.xml create mode 100644 conversations/src/main/res/values-de/strings.xml create mode 100644 conversations/src/main/res/values-es/arrays.xml create mode 100644 conversations/src/main/res/values-es/strings.xml create mode 100644 conversations/src/main/res/values-eu/arrays.xml create mode 100644 conversations/src/main/res/values-eu/strings.xml create mode 100644 conversations/src/main/res/values-fr/arrays.xml create mode 100644 conversations/src/main/res/values-fr/strings.xml create mode 100644 conversations/src/main/res/values-gl/arrays.xml create mode 100644 conversations/src/main/res/values-gl/strings.xml create mode 100644 conversations/src/main/res/values-it/arrays.xml create mode 100644 conversations/src/main/res/values-it/strings.xml create mode 100644 conversations/src/main/res/values-iw/arrays.xml create mode 100644 conversations/src/main/res/values-iw/strings.xml create mode 100644 conversations/src/main/res/values-nl/arrays.xml create mode 100644 conversations/src/main/res/values-nl/strings.xml create mode 100644 conversations/src/main/res/values-ru/arrays.xml create mode 100644 conversations/src/main/res/values-ru/strings.xml create mode 100644 conversations/src/main/res/values-sv/arrays.xml create mode 100644 conversations/src/main/res/values-sv/strings.xml create mode 100644 conversations/src/main/res/values-zh-rCN/arrays.xml create mode 100644 conversations/src/main/res/values-zh-rCN/strings.xml create mode 100644 conversations/src/main/res/values-zh-rTW/arrays.xml create mode 100644 conversations/src/main/res/values-zh-rTW/strings.xml create mode 100644 conversations/src/main/res/values/arrays.xml create mode 100644 conversations/src/main/res/values/attrs.xml create mode 100644 conversations/src/main/res/values/colors.xml create mode 100644 conversations/src/main/res/values/strings.xml create mode 100644 conversations/src/main/res/values/styles.xml create mode 100644 conversations/src/main/res/values/themes.xml create mode 100644 conversations/src/main/res/xml/preferences.xml create mode 100755 gradlew create mode 100644 gradlew.bat create mode 160000 memorizingTrustManager create mode 160000 minidns create mode 160000 openpgpapilib create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e7f0d7fe6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +.classpath +*.swp +.settings + +# https://github.com/github/gitignore/blob/master/Gradle.gitignore +.gradle/ +gradle/ +build/ +# Ignore Gradle GUI config +gradle-app.setting + +# https://github.com/github/gitignore/blob/master/Android.gitignore +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +*.iml +.idea + +import-summary.txt + +*.jar diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e92902458 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "minidns"] + path = minidns + url = https://github.com/rtreffer/minidns.git +[submodule "openpgpapilib"] + path = openpgpapilib + url = https://github.com/open-keychain/openpgp-api-lib.git +[submodule "memorizingTrustManager"] + path = memorizingTrustManager + url = https://github.com/iNPUTmice/MemorizingTrustManager.git diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..8942ba1c2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +// Top-level build file where you can add configuration options common to all +// sub-projects/modules. +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.12.2' + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/conversations/build.gradle b/conversations/build.gradle new file mode 100644 index 000000000..08060e59c --- /dev/null +++ b/conversations/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 19 + buildToolsVersion "20.0.0" + + defaultConfig { + applicationId "eu.siacs.conversations" + minSdkVersion 14 + targetSdkVersion 19 + } + + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +dependencies { + compile project(':minidns') + compile project(':openpgpapilib') + compile project(':memorizingTrustManager') + compile files('libs/android-support-v13.jar') + compile files('libs/bcprov-jdk15on-150.jar') + compile files('libs/otr4j-0.10.jar') +} diff --git a/conversations/src/main/AndroidManifest.xml b/conversations/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a66de8ba6 --- /dev/null +++ b/conversations/src/main/AndroidManifest.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conversations/src/main/java/eu/siacs/conversations/Config.java b/conversations/src/main/java/eu/siacs/conversations/Config.java new file mode 100644 index 000000000..1725eca69 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/Config.java @@ -0,0 +1,25 @@ +package eu.siacs.conversations; + +import android.graphics.Bitmap; + +public final class Config { + + public static final String LOGTAG = "conversations"; + + public static final int PING_MAX_INTERVAL = 300; + public static final int PING_MIN_INTERVAL = 30; + public static final int PING_TIMEOUT = 10; + public static final int CONNECT_TIMEOUT = 90; + public static final int CARBON_GRACE_PERIOD = 60; + + public static final int AVATAR_SIZE = 192; + public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; + + public static final int MESSAGE_MERGE_WINDOW = 20; + + public static final boolean PARSE_EMOTICONS = false; + + private Config() { + + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java new file mode 100644 index 000000000..e0bd0e793 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java @@ -0,0 +1,231 @@ +package eu.siacs.conversations.crypto; + +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 org.json.JSONException; +import org.json.JSONObject; + +import android.util.Log; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +import net.java.otr4j.OtrEngineHost; +import net.java.otr4j.OtrException; +import net.java.otr4j.OtrPolicy; +import net.java.otr4j.OtrPolicyImpl; +import net.java.otr4j.session.InstanceTag; +import net.java.otr4j.session.SessionID; + +public class OtrEngine implements OtrEngineHost { + + private Account account; + private OtrPolicy otrPolicy; + private KeyPair keyPair; + private XmppConnectionService mXmppConnectionService; + + public OtrEngine(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(JSONObject keys) { + if (keys == null) { + return null; + } + 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 (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } + + } + + @Override + public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void finishedSessionMessage(SessionID arg0, String arg1) + throws OtrException { + + } + + @Override + public String getFallbackMessage(SessionID arg0) { + return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that"; + } + + @Override + public byte[] getLocalFingerprintRaw(SessionID arg0) { + // TODO Auto-generated method stub + 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.getFullJid()); + if (session.getUserID().isEmpty()) { + packet.setTo(session.getAccountID()); + } else { + packet.setTo(session.getAccountID() + "/" + session.getUserID()); + } + packet.setBody(body); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setType(MessagePacket.TYPE_CHAT); + account.getXmppConnection().sendMessagePacket(packet); + } + + @Override + public void messageFromAnotherInstanceReceived(SessionID id) { + Log.d(Config.LOGTAG, + "unreadable message received from " + id.getAccountID()); + } + + @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 { + // TODO Auto-generated method stub + + } + + @Override + public void smpAborted(SessionID arg0) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void smpError(SessionID arg0, int arg1, boolean arg2) + throws OtrException { + throw new OtrException(new Exception("smp error")); + } + + @Override + public void unencryptedMessageReceived(SessionID arg0, String arg1) + throws OtrException { + throw new OtrException(new Exception("unencrypted message received")); + } + + @Override + public void unreadableMessageReceived(SessionID arg0) throws OtrException { + throw new OtrException(new Exception("unreadable message received")); + } + + @Override + public void unverify(SessionID arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void verify(SessionID arg0, String arg1, boolean arg2) { + // TODO Auto-generated method stub + + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java new file mode 100644 index 000000000..2696c7d2a --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -0,0 +1,385 @@ +package eu.siacs.conversations.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.UiCallback; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.util.Log; + +public class PgpEngine { + private OpenPgpApi api; + private XmppConnectionService mXmppConnectionService; + + public PgpEngine(OpenPgpApi api, XmppConnectionService service) { + this.api = api; + this.mXmppConnectionService = service; + } + + public void decrypt(final Message message, + final UiCallback callback) { + Log.d(Config.LOGTAG, "decrypting message " + message.getUuid()); + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + if (message.getType() == Message.TYPE_TEXT) { + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + message.setBody(os.toString()); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + callback.success(message); + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + return; + } + + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + OpenPgpError error = result + .getParcelableExtra(OpenPgpApi.RESULT_ERROR); + Log.d(Config.LOGTAG, + "openpgp error: " + error.getMessage()); + callback.error(R.string.openpgp_error, message); + return; + default: + return; + } + } + }); + } else if (message.getType() == Message.TYPE_IMAGE) { + try { + final DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + final DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + outputFile.createNewFile(); + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile( + outputFile.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(Long.toString(outputFile.getSize()) + + ',' + imageWidth + ',' + imageHeight); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + PgpEngine.this.mXmppConnectionService + .updateMessage(message); + ; + callback.success(message); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + return; + default: + return; + } + } + }); + } catch (FileNotFoundException e) { + callback.error(R.string.error_decrypting_file, message); + } catch (IOException e) { + callback.error(R.string.error_decrypting_file, message); + } + + } + } + + public void encrypt(final Message message, + final UiCallback callback) { + + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_ENCRYPT); + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + long[] keys = { message.getConversation().getContact() + .getPgpKeyId() }; + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); + } else { + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation() + .getMucOptions().getPgpKeyIds()); + } + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + + if (message.getType() == Message.TYPE_TEXT) { + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + StringBuilder encryptedMessageBody = new StringBuilder(); + String[] lines = os.toString().split("\n"); + for (int i = 2; i < lines.length - 1; ++i) { + if (!lines[i].contains("Version")) { + encryptedMessageBody.append(lines[i].trim()); + } + } + message.setEncryptedBody(encryptedMessageBody + .toString()); + callback.success(message); + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + } + + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } else if (message.getType() == Message.TYPE_IMAGE) { + try { + DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + outputFile.createNewFile(); + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(message); + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } catch (FileNotFoundException e) { + Log.d(Config.LOGTAG, "file not found: " + e.getMessage()); + } catch (IOException e) { + Log.d(Config.LOGTAG, "io exception during file encrypt"); + } + } + } + + public long fetchKeyId(Account account, String status, String signature) { + if ((signature == null) || (api == null)) { + return 0; + } + if (status == null) { + status = ""; + } + StringBuilder pgpSig = new StringBuilder(); + pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(status); + pgpSig.append('\n'); + pgpSig.append("-----BEGIN PGP SIGNATURE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(signature.replace("\n", "").trim()); + pgpSig.append('\n'); + pgpSig.append("-----END PGP SIGNATURE-----"); + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Intent result = api.executeApi(params, is, os); + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + OpenPgpSignatureResult sigResult = result + .getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); + if (sigResult != null) { + return sigResult.getKeyId(); + } else { + return 0; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + return 0; + case OpenPgpApi.RESULT_CODE_ERROR: + Log.d(Config.LOGTAG, + "openpgp error: " + + ((OpenPgpError) result + .getParcelableExtra(OpenPgpApi.RESULT_ERROR)) + .getMessage()); + return 0; + } + return 0; + } + + public void generateSignature(final Account account, String status, + final UiCallback callback) { + Intent params = new Intent(); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + params.setAction(OpenPgpApi.ACTION_SIGN); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + InputStream is = new ByteArrayInputStream(status.getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + StringBuilder signatureBuilder = new StringBuilder(); + try { + os.flush(); + String[] lines = os.toString().split("\n"); + boolean sig = false; + for (String line : lines) { + if (sig) { + if (line.contains("END PGP SIGNATURE")) { + sig = false; + } else { + if (!line.contains("Version")) { + signatureBuilder.append(line.trim()); + } + } + } + if (line.contains("BEGIN PGP SIGNATURE")) { + sig = true; + } + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, account); + return; + } + account.setKey("pgp_signature", signatureBuilder.toString()); + callback.success(account); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + account); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, account); + return; + } + } + }); + } + + public void hasKey(final Contact contact, final UiCallback callback) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + api.executeApiAsync(params, null, null, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(contact); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + contact); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, contact); + return; + } + } + }); + } + + public PendingIntent getIntentForKey(Contact contact) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } + + public PendingIntent getIntentForKey(Account account, long pgpKeyId) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java new file mode 100644 index 000000000..92b8a7298 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java @@ -0,0 +1,21 @@ +package eu.siacs.conversations.entities; + +import android.content.ContentValues; + +public abstract class AbstractEntity { + + public static final String UUID = "uuid"; + + protected String uuid; + + public String getUuid() { + return this.uuid; + } + + public abstract ContentValues getContentValues(); + + public boolean equals(AbstractEntity entity) { + return this.getUuid().equals(entity.getUuid()); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Account.java b/conversations/src/main/java/eu/siacs/conversations/entities/Account.java new file mode 100644 index 000000000..80a9d62f9 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Account.java @@ -0,0 +1,399 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; + +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; + +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.OtrEngine; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.XmppConnection; +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +public class Account extends AbstractEntity { + + public static final String TABLENAME = "accounts"; + + public static final String USERNAME = "username"; + public static final String SERVER = "server"; + public static final String PASSWORD = "password"; + public static final String OPTIONS = "options"; + public static final String ROSTERVERSION = "rosterversion"; + public static final String KEYS = "keys"; + public static final String AVATAR = "avatar"; + + public static final int OPTION_USETLS = 0; + public static final int OPTION_DISABLED = 1; + public static final int OPTION_REGISTER = 2; + public static final int OPTION_USECOMPRESSION = 3; + + public static final int STATUS_CONNECTING = 0; + public static final int STATUS_DISABLED = -2; + public static final int STATUS_OFFLINE = -1; + public static final int STATUS_ONLINE = 1; + public static final int STATUS_NO_INTERNET = 2; + public static final int STATUS_UNAUTHORIZED = 3; + public static final int STATUS_SERVER_NOT_FOUND = 5; + + public static final int STATUS_REGISTRATION_FAILED = 7; + public static final int STATUS_REGISTRATION_CONFLICT = 8; + public static final int STATUS_REGISTRATION_SUCCESSFULL = 9; + public static final int STATUS_REGISTRATION_NOT_SUPPORTED = 10; + + protected String username; + protected String server; + protected String password; + protected int options = 0; + protected String rosterVersion; + protected String resource = "mobile"; + protected int status = -1; + protected JSONObject keys = new JSONObject(); + protected String avatar; + + protected boolean online = false; + + private OtrEngine otrEngine = null; + private XmppConnection xmppConnection = null; + private Presences presences = new Presences(); + private long mEndGracePeriod = 0L; + private String otrFingerprint; + private Roster roster = null; + + private List bookmarks = new CopyOnWriteArrayList(); + public List pendingConferenceJoins = new CopyOnWriteArrayList(); + public List pendingConferenceLeaves = new CopyOnWriteArrayList(); + + public Account() { + this.uuid = "0"; + } + + public Account(String username, String server, String password) { + this(java.util.UUID.randomUUID().toString(), username, server, + password, 0, null, "", null); + } + + public Account(String uuid, String username, String server, + String password, int options, String rosterVersion, String keys, + String avatar) { + this.uuid = uuid; + this.username = username; + this.server = server; + this.password = password; + this.options = options; + this.rosterVersion = rosterVersion; + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + + } + this.avatar = avatar; + } + + public boolean isOptionSet(int option) { + return ((options & (1 << option)) != 0); + } + + public void setOption(int option, boolean value) { + if (value) { + this.options |= 1 << option; + } else { + this.options &= ~(1 << option); + } + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + if (isOptionSet(OPTION_DISABLED)) { + return STATUS_DISABLED; + } else { + return this.status; + } + } + + public boolean errorStatus() { + int s = getStatus(); + return (s == STATUS_REGISTRATION_FAILED + || s == STATUS_REGISTRATION_CONFLICT + || s == STATUS_REGISTRATION_NOT_SUPPORTED + || s == STATUS_SERVER_NOT_FOUND || s == STATUS_UNAUTHORIZED); + } + + public boolean hasErrorStatus() { + if (getXmppConnection() == null) { + return false; + } else { + return getStatus() > STATUS_NO_INTERNET + && (getXmppConnection().getAttempt() >= 2); + } + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getResource() { + return this.resource; + } + + public String getJid() { + return username.toLowerCase(Locale.getDefault()) + "@" + + server.toLowerCase(Locale.getDefault()); + } + + public JSONObject getKeys() { + return keys; + } + + public String getSSLFingerprint() { + if (keys.has("ssl_cert")) { + try { + return keys.getString("ssl_cert"); + } catch (JSONException e) { + return null; + } + } else { + return null; + } + } + + public void setSSLCertFingerprint(String fingerprint) { + this.setKey("ssl_cert", fingerprint); + } + + public boolean setKey(String keyName, String keyValue) { + try { + this.keys.put(keyName, keyValue); + return true; + } catch (JSONException e) { + return false; + } + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(USERNAME, username); + values.put(SERVER, server); + values.put(PASSWORD, password); + values.put(OPTIONS, options); + values.put(KEYS, this.keys.toString()); + values.put(ROSTERVERSION, rosterVersion); + values.put(AVATAR, avatar); + return values; + } + + public static Account fromCursor(Cursor cursor) { + return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(USERNAME)), + cursor.getString(cursor.getColumnIndex(SERVER)), + cursor.getString(cursor.getColumnIndex(PASSWORD)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), + cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), + cursor.getString(cursor.getColumnIndex(KEYS)), + cursor.getString(cursor.getColumnIndex(AVATAR))); + } + + public OtrEngine getOtrEngine(XmppConnectionService context) { + if (otrEngine == null) { + otrEngine = new OtrEngine(context, this); + } + return this.otrEngine; + } + + public XmppConnection getXmppConnection() { + return this.xmppConnection; + } + + public void setXmppConnection(XmppConnection connection) { + this.xmppConnection = connection; + } + + public String getFullJid() { + return this.getJid() + "/" + this.resource; + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + DSAPublicKey pubkey = (DSAPublicKey) this.otrEngine + .getPublicKey(); + if (pubkey == null) { + return null; + } + StringBuilder builder = new StringBuilder( + new OtrCryptoEngineImpl().getFingerprint(pubkey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public String getRosterVersion() { + if (this.rosterVersion == null) { + return ""; + } else { + return this.rosterVersion; + } + } + + public void setRosterVersion(String version) { + this.rosterVersion = version; + } + + public String getOtrFingerprint(XmppConnectionService service) { + this.getOtrEngine(service); + return this.getOtrFingerprint(); + } + + public void updatePresence(String resource, int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(String resource) { + this.presences.removePresence(resource); + } + + public void clearPresences() { + this.presences = new Presences(); + } + + public int countPresences() { + return this.presences.size(); + } + + public String getPgpSignature() { + if (keys.has("pgp_signature")) { + try { + return keys.getString("pgp_signature"); + } catch (JSONException e) { + return null; + } + } else { + return null; + } + } + + public Roster getRoster() { + if (this.roster == null) { + this.roster = new Roster(this); + } + return this.roster; + } + + public void setBookmarks(List bookmarks) { + this.bookmarks = bookmarks; + } + + public List getBookmarks() { + return this.bookmarks; + } + + public boolean hasBookmarkFor(String conferenceJid) { + for (Bookmark bmark : this.bookmarks) { + if (bmark.getJid().equals(conferenceJid)) { + return true; + } + } + return false; + } + + public boolean setAvatar(String filename) { + if (this.avatar != null && this.avatar.equals(filename)) { + return false; + } else { + this.avatar = filename; + return true; + } + } + + public String getAvatar() { + return this.avatar; + } + + public int getReadableStatusId() { + switch (getStatus()) { + + case Account.STATUS_DISABLED: + return R.string.account_status_disabled; + case Account.STATUS_ONLINE: + return R.string.account_status_online; + case Account.STATUS_CONNECTING: + return R.string.account_status_connecting; + case Account.STATUS_OFFLINE: + return R.string.account_status_offline; + case Account.STATUS_UNAUTHORIZED: + return R.string.account_status_unauthorized; + case Account.STATUS_SERVER_NOT_FOUND: + return R.string.account_status_not_found; + case Account.STATUS_NO_INTERNET: + return R.string.account_status_no_internet; + case Account.STATUS_REGISTRATION_FAILED: + return R.string.account_status_regis_fail; + case Account.STATUS_REGISTRATION_CONFLICT: + return R.string.account_status_regis_conflict; + case Account.STATUS_REGISTRATION_SUCCESSFULL: + return R.string.account_status_regis_success; + case Account.STATUS_REGISTRATION_NOT_SUPPORTED: + return R.string.account_status_regis_not_sup; + default: + return R.string.account_status_unknown; + } + } + + public void activateGracePeriod() { + this.mEndGracePeriod = SystemClock.elapsedRealtime() + + (Config.CARBON_GRACE_PERIOD * 1000); + } + + public void deactivateGracePeriod() { + this.mEndGracePeriod = 0L; + } + + public boolean inGracePeriod() { + return SystemClock.elapsedRealtime() < this.mEndGracePeriod; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java new file mode 100644 index 000000000..dd9e805c2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -0,0 +1,137 @@ +package eu.siacs.conversations.entities; + +import java.util.Locale; + +import eu.siacs.conversations.xml.Element; + +public class Bookmark extends Element implements ListItem { + + private Account account; + private Conversation mJoinedConversation; + + public Bookmark(Account account, String jid) { + super("conference"); + this.setAttribute("jid", jid); + this.account = account; + } + + private Bookmark(Account account) { + super("conference"); + this.account = account; + } + + public static Bookmark parse(Element element, Account account) { + Bookmark bookmark = new Bookmark(account); + bookmark.setAttributes(element.getAttributes()); + bookmark.setChildren(element.getChildren()); + return bookmark; + } + + public void setAutojoin(boolean autojoin) { + if (autojoin) { + this.setAttribute("autojoin", "true"); + } else { + this.setAttribute("autojoin", "false"); + } + } + + public void setName(String name) { + this.name = name; + } + + public void setNick(String nick) { + Element element = this.findChild("nick"); + if (element == null) { + element = this.addChild("nick"); + } + element.setContent(nick); + } + + public void setPassword(String password) { + Element element = this.findChild("password"); + if (element != null) { + element.setContent(password); + } + } + + @Override + public int compareTo(ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } + + @Override + public String getDisplayName() { + if (this.mJoinedConversation != null + && (this.mJoinedConversation.getMucOptions().getSubject() != null)) { + return this.mJoinedConversation.getMucOptions().getSubject(); + } else if (getName() != null) { + return getName(); + } else { + return this.getJid().split("@")[0]; + } + } + + @Override + public String getJid() { + String jid = this.getAttribute("jid"); + if (jid != null) { + return jid.toLowerCase(Locale.US); + } else { + return null; + } + } + + public String getNick() { + Element nick = this.findChild("nick"); + if (nick != null) { + return nick.getContent(); + } else { + return null; + } + } + + public boolean autojoin() { + String autojoin = this.getAttribute("autojoin"); + return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin + .equalsIgnoreCase("1"))); + } + + public String getPassword() { + Element password = this.findChild("password"); + if (password != null) { + return password.getContent(); + } else { + return null; + } + } + + public boolean match(String needle) { + return needle == null + || getJid().contains(needle.toLowerCase(Locale.US)) + || getDisplayName().toLowerCase(Locale.US).contains( + needle.toLowerCase(Locale.US)); + } + + public Account getAccount() { + return this.account; + } + + public void setConversation(Conversation conversation) { + this.mJoinedConversation = conversation; + } + + public Conversation getConversation() { + return this.mJoinedConversation; + } + + public String getName() { + return this.getAttribute("name"); + } + + public void unregisterConversation() { + if (this.mJoinedConversation != null) { + this.mJoinedConversation.deregisterWithBookmark(); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java b/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java new file mode 100644 index 000000000..60c31a424 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -0,0 +1,367 @@ +package eu.siacs.conversations.entities; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.xml.Element; +import android.content.ContentValues; +import android.database.Cursor; + +public class Contact implements ListItem { + public static final String TABLENAME = "contacts"; + + public static final String SYSTEMNAME = "systemname"; + public static final String SERVERNAME = "servername"; + public static final String JID = "jid"; + public static final String OPTIONS = "options"; + public static final String SYSTEMACCOUNT = "systemaccount"; + public static final String PHOTOURI = "photouri"; + public static final String KEYS = "pgpkey"; + public static final String ACCOUNT = "accountUuid"; + public static final String AVATAR = "avatar"; + + protected String accountUuid; + protected String systemName; + protected String serverName; + protected String presenceName; + protected String jid; + protected int subscription = 0; + protected String systemAccount; + protected String photoUri; + protected String avatar; + protected JSONObject keys = new JSONObject(); + protected Presences presences = new Presences(); + + protected Account account; + + protected boolean inRoster = true; + + public Lastseen lastseen = new Lastseen(); + + public Contact(String account, String systemName, String serverName, + String jid, int subscription, String photoUri, + String systemAccount, String keys, String avatar) { + this.accountUuid = account; + this.systemName = systemName; + this.serverName = serverName; + this.jid = jid; + this.subscription = subscription; + this.photoUri = photoUri; + this.systemAccount = systemAccount; + if (keys == null) { + keys = ""; + } + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + this.keys = new JSONObject(); + } + this.avatar = avatar; + } + + public Contact(String jid) { + this.jid = jid; + } + + public String getDisplayName() { + if (this.systemName != null) { + return this.systemName; + } else if (this.serverName != null) { + return this.serverName; + } else if (this.presenceName != null) { + return this.presenceName; + } else { + return this.jid.split("@")[0]; + } + } + + public String getProfilePhoto() { + return this.photoUri; + } + + public String getJid() { + return this.jid.toLowerCase(Locale.getDefault()); + } + + public boolean match(String needle) { + return needle == null + || jid.contains(needle.toLowerCase()) + || getDisplayName().toLowerCase() + .contains(needle.toLowerCase()); + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar); + return values; + } + + public static Contact fromCursor(Cursor cursor) { + return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), + cursor.getString(cursor.getColumnIndex(SERVERNAME)), + cursor.getString(cursor.getColumnIndex(JID)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), + cursor.getString(cursor.getColumnIndex(PHOTOURI)), + cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), + cursor.getString(cursor.getColumnIndex(KEYS)), + cursor.getString(cursor.getColumnIndex(AVATAR))); + } + + public int getSubscription() { + return this.subscription; + } + + public void setSystemAccount(String account) { + this.systemAccount = account; + } + + public void setAccount(Account account) { + this.account = account; + this.accountUuid = account.getUuid(); + } + + public Account getAccount() { + return this.account; + } + + public Presences getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(String resource) { + this.presences.removePresence(resource); + } + + public void clearPresences() { + this.presences.clearPresences(); + this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); + } + + public int getMostAvailableStatus() { + return this.presences.getMostAvailableStatus(); + } + + public void setPresences(Presences pres) { + this.presences = pres; + } + + public void setPhotoUri(String uri) { + this.photoUri = uri; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setSystemName(String systemName) { + this.systemName = systemName; + } + + public void setPresenceName(String presenceName) { + this.presenceName = presenceName; + } + + public String getSystemAccount() { + return systemAccount; + } + + public Set getOtrFingerprints() { + Set set = new HashSet(); + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray fingerprints = this.keys + .getJSONArray("otr_fingerprints"); + for (int i = 0; i < fingerprints.length(); ++i) { + set.add(fingerprints.getString(i)); + } + } + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return set; + } + + public void addOtrFingerprint(String print) { + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + } catch (JSONException e) { + + } + } + + public void setPgpKeyId(long keyId) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (JSONException e) { + + } + } + + public long getPgpKeyId() { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } + } else { + return 0; + } + } + + public void setOption(int option) { + this.subscription |= 1 << option; + } + + public void resetOption(int option) { + this.subscription &= ~(1 << option); + } + + public boolean getOption(int option) { + return ((this.subscription & (1 << option)) != 0); + } + + public boolean showInRoster() { + return (this.getOption(Contact.Options.IN_ROSTER) && (!this + .getOption(Contact.Options.DIRTY_DELETE))) + || (this.getOption(Contact.Options.DIRTY_PUSH)); + } + + public void parseSubscriptionFromElement(Element item) { + String ask = item.getAttribute("ask"); + String subscription = item.getAttribute("subscription"); + + if (subscription != null) { + if (subscription.equals("to")) { + this.resetOption(Contact.Options.FROM); + this.setOption(Contact.Options.TO); + } else if (subscription.equals("from")) { + this.resetOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + this.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else if (subscription.equals("both")) { + this.setOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + this.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else if (subscription.equals("none")) { + this.resetOption(Contact.Options.FROM); + this.resetOption(Contact.Options.TO); + } + } + + // do NOT override asking if pending push request + if (!this.getOption(Contact.Options.DIRTY_PUSH)) { + if ((ask != null) && (ask.equals("subscribe"))) { + this.setOption(Contact.Options.ASKING); + } else { + this.resetOption(Contact.Options.ASKING); + } + } + } + + public Element asElement() { + Element item = new Element("item"); + item.setAttribute("jid", this.jid); + if (this.serverName != null) { + item.setAttribute("name", this.serverName); + } + return item; + } + + public class Options { + public static final int TO = 0; + public static final int FROM = 1; + public static final int ASKING = 2; + public static final int PREEMPTIVE_GRANT = 3; + public static final int IN_ROSTER = 4; + public static final int PENDING_SUBSCRIPTION_REQUEST = 5; + public static final int DIRTY_PUSH = 6; + public static final int DIRTY_DELETE = 7; + } + + public class Lastseen { + public long time = 0; + public String presence = null; + } + + @Override + public int compareTo(ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } + + public String getServer() { + String[] split = getJid().split("@"); + if (split.length >= 2) { + return split[1]; + } else { + return null; + } + } + + public boolean setAvatar(String filename) { + if (this.avatar != null && this.avatar.equals(filename)) { + return false; + } else { + this.avatar = filename; + return true; + } + } + + public String getAvatar() { + return this.avatar; + } + + public boolean deleteOtrFingerprint(String fingerprint) { + 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 trusted() { + return getOption(Options.FROM) && getOption(Options.TO); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java b/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java new file mode 100644 index 000000000..9d4c36db5 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -0,0 +1,500 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.services.XmppConnectionService; + +import net.java.otr4j.OtrException; +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +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 android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +public class Conversation extends AbstractEntity { + public static final String TABLENAME = "conversations"; + + public static final int STATUS_AVAILABLE = 0; + public static final int STATUS_ARCHIVED = 1; + public static final int STATUS_DELETED = 2; + + public static final int MODE_MULTI = 1; + public static final int MODE_SINGLE = 0; + + public static final String NAME = "name"; + public static final String ACCOUNT = "accountUuid"; + public static final String CONTACT = "contactUuid"; + public static final String CONTACTJID = "contactJid"; + public static final String STATUS = "status"; + public static final String CREATED = "created"; + public static final String MODE = "mode"; + public static final String ATTRIBUTES = "attributes"; + + public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; + public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; + public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; + + private String name; + private String contactUuid; + private String accountUuid; + private String contactJid; + private int status; + private long created; + private int mode; + + private JSONObject attributes = new JSONObject(); + + private String nextPresence; + + protected ArrayList messages = new ArrayList(); + protected Account account = null; + + private transient SessionImpl otrSession; + + private transient String otrFingerprint = null; + + private String nextMessage; + + private transient MucOptions mucOptions = null; + + // private transient String latestMarkableMessageId; + + private byte[] symmetricKey; + + private Bookmark bookmark; + + public Conversation(String name, Account account, String contactJid, + int mode) { + this(java.util.UUID.randomUUID().toString(), name, null, account + .getUuid(), contactJid, System.currentTimeMillis(), + STATUS_AVAILABLE, mode, ""); + this.account = account; + } + + public Conversation(String uuid, String name, String contactUuid, + String accountUuid, String contactJid, long created, int status, + int mode, String attributes) { + this.uuid = uuid; + this.name = name; + this.contactUuid = contactUuid; + this.accountUuid = accountUuid; + this.contactJid = contactJid; + this.created = created; + this.status = status; + this.mode = mode; + try { + if (attributes == null) { + attributes = new String(); + } + this.attributes = new JSONObject(attributes); + } catch (JSONException e) { + this.attributes = new JSONObject(); + } + } + + public List getMessages() { + return messages; + } + + public boolean isRead() { + if ((this.messages == null) || (this.messages.size() == 0)) + return true; + return this.messages.get(this.messages.size() - 1).isRead(); + } + + public void markRead() { + if (this.messages == null) { + return; + } + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (messages.get(i).isRead()) { + break; + } + this.messages.get(i).markRead(); + } + } + + public String getLatestMarkableMessageId() { + if (this.messages == null) { + return null; + } + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED + && this.messages.get(i).markable) { + if (this.messages.get(i).isRead()) { + return null; + } else { + return this.messages.get(i).getRemoteMsgId(); + } + } + } + return null; + } + + public Message getLatestMessage() { + if ((this.messages == null) || (this.messages.size() == 0)) { + Message message = new Message(this, "", Message.ENCRYPTION_NONE); + message.setTime(getCreated()); + return message; + } else { + Message message = this.messages.get(this.messages.size() - 1); + message.setConversation(this); + return message; + } + } + + public void setMessages(ArrayList msgs) { + this.messages = msgs; + } + + public String getName() { + if (getMode() == MODE_MULTI && getMucOptions().getSubject() != null) { + return getMucOptions().getSubject(); + } else if (getMode() == MODE_MULTI && bookmark != null + && bookmark.getName() != null) { + return bookmark.getName(); + } else { + return this.getContact().getDisplayName(); + } + } + + public String getProfilePhotoString() { + return this.getContact().getProfilePhoto(); + } + + public String getAccountUuid() { + return this.accountUuid; + } + + public Account getAccount() { + return this.account; + } + + public Contact getContact() { + return this.account.getRoster().getContact(this.contactJid); + } + + public void setAccount(Account account) { + this.account = account; + } + + public String getContactJid() { + return this.contactJid; + } + + public int getStatus() { + return this.status; + } + + public long getCreated() { + return this.created; + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(NAME, name); + values.put(CONTACT, contactUuid); + values.put(ACCOUNT, accountUuid); + values.put(CONTACTJID, contactJid); + values.put(CREATED, created); + values.put(STATUS, status); + values.put(MODE, mode); + values.put(ATTRIBUTES, attributes.toString()); + return values; + } + + public static Conversation fromCursor(Cursor cursor) { + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(NAME)), + cursor.getString(cursor.getColumnIndex(CONTACT)), + cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(CONTACTJID)), + cursor.getLong(cursor.getColumnIndex(CREATED)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(MODE)), + cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); + } + + public void setStatus(int status) { + this.status = status; + } + + public int getMode() { + return this.mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + public SessionImpl startOtrSession(XmppConnectionService service, + String presence, boolean sendStart) { + if (this.otrSession != null) { + return this.otrSession; + } else { + SessionID sessionId = new SessionID(this.getContactJid().split("/", + 2)[0], presence, "xmpp"); + this.otrSession = new SessionImpl(sessionId, getAccount() + .getOtrEngine(service)); + 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; + } + + public void startOtrIfNeeded() { + if (this.otrSession != null + && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { + try { + this.otrSession.startSession(); + } catch (OtrException e) { + this.resetOtrSession(); + } + } + } + + 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 String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + if (getOtrSession() == null) { + return ""; + } + DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession() + .getRemotePublicKey(); + StringBuilder builder = new StringBuilder( + new OtrCryptoEngineImpl().getFingerprint(remotePubKey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public synchronized MucOptions getMucOptions() { + if (this.mucOptions == null) { + this.mucOptions = new MucOptions(this); + } + return this.mucOptions; + } + + public void resetMucOptions() { + this.mucOptions = null; + } + + public void setContactJid(String jid) { + this.contactJid = jid; + } + + public void setNextPresence(String presence) { + this.nextPresence = presence; + } + + public String getNextPresence() { + return this.nextPresence; + } + + public int getLatestEncryption() { + int latestEncryption = this.getLatestMessage().getEncryption(); + if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) + || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { + return Message.ENCRYPTION_PGP; + } else { + return latestEncryption; + } + } + + public int getNextEncryption(boolean force) { + int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); + if (next == -1) { + int latest = this.getLatestEncryption(); + if (latest == Message.ENCRYPTION_NONE) { + if (force && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else if (getContact().getPresences().size() == 1) { + if (getContact().getOtrFingerprints().size() >= 1) { + return Message.ENCRYPTION_OTR; + } else { + return latest; + } + } else { + return latest; + } + } else { + return latest; + } + } + if (next == Message.ENCRYPTION_NONE && force + && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else { + return next; + } + } + + public void setNextEncryption(int encryption) { + this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption)); + } + + public String getNextMessage() { + if (this.nextMessage == null) { + return ""; + } else { + return this.nextMessage; + } + } + + public void setNextMessage(String message) { + this.nextMessage = message; + } + + public void setSymmetricKey(byte[] key) { + this.symmetricKey = key; + } + + public byte[] getSymmetricKey() { + return this.symmetricKey; + } + + public void setBookmark(Bookmark bookmark) { + this.bookmark = bookmark; + this.bookmark.setConversation(this); + } + + public void deregisterWithBookmark() { + if (this.bookmark != null) { + this.bookmark.setConversation(null); + } + } + + public Bookmark getBookmark() { + return this.bookmark; + } + + public boolean hasDuplicateMessage(Message message) { + for (int i = this.getMessages().size() - 1; i >= 0; --i) { + if (this.messages.get(i).equals(message)) { + return true; + } + } + return false; + } + + public void setMutedTill(long value) { + this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); + } + + public boolean isMuted() { + return SystemClock.elapsedRealtime() < this.getLongAttribute( + ATTRIBUTE_MUTED_TILL, 0); + } + + public boolean setAttribute(String key, String value) { + try { + this.attributes.put(key, value); + return true; + } catch (JSONException e) { + return false; + } + } + + public String getAttribute(String key) { + try { + return this.attributes.getString(key); + } catch (JSONException e) { + return null; + } + } + + public int getIntAttribute(String key, int defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public long getLongAttribute(String key, long defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public void add(Message message) { + message.setConversation(this); + synchronized (this.messages) { + this.messages.add(message); + } + } + + public void addAll(int index, List messages) { + synchronized (this.messages) { + this.messages.addAll(index, messages); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java b/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java new file mode 100644 index 000000000..70516b204 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java @@ -0,0 +1,21 @@ +package eu.siacs.conversations.entities; + +public interface Downloadable { + + public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" }; + public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" }; + + public static final int STATUS_UNKNOWN = 0x200; + public static final int STATUS_CHECKING = 0x201; + public static final int STATUS_FAILED = 0x202; + public static final int STATUS_OFFER = 0x203; + public static final int STATUS_DOWNLOADING = 0x204; + public static final int STATUS_DELETED = 0x205; + public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206; + + public boolean start(); + + public int getStatus(); + + public long getFileSize(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java new file mode 100644 index 000000000..1605c75b4 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -0,0 +1,154 @@ +package eu.siacs.conversations.entities; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; +import android.util.Log; + +public class DownloadableFile extends File { + + private static final long serialVersionUID = 2247012619505115863L; + + private long expectedSize = 0; + private String sha1sum; + private Key aeskey; + + private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; + + public DownloadableFile(String path) { + super(path); + } + + public long getSize() { + return super.length(); + } + + public long getExpectedSize() { + if (this.aeskey != null) { + if (this.expectedSize == 0) { + return 0; + } else { + return (this.expectedSize / 16 + 1) * 16; + } + } else { + return this.expectedSize; + } + } + + public void setExpectedSize(long size) { + this.expectedSize = size; + } + + public String getSha1Sum() { + return this.sha1sum; + } + + public void setSha1Sum(String sum) { + this.sha1sum = sum; + } + + public void setKey(byte[] key) { + if (key.length == 48) { + byte[] secretKey = new byte[32]; + byte[] iv = new byte[16]; + System.arraycopy(key, 0, iv, 0, 16); + System.arraycopy(key, 16, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + this.iv = iv; + } else if (key.length >= 32) { + byte[] secretKey = new byte[32]; + System.arraycopy(key, 0, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else if (key.length >= 16) { + byte[] secretKey = new byte[16]; + System.arraycopy(key, 0, secretKey, 0, 16); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } + } + + public Key getKey() { + return this.aeskey; + } + + public InputStream createInputStream() { + if (this.getKey() == null) { + try { + return new FileInputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted input stream"); + return new CipherInputStream(new FileInputStream(this), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } + + public OutputStream createOutputStream() { + if (this.getKey() == null) { + try { + return new FileOutputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(this.iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted output stream"); + return new CipherOutputStream(new FileOutputStream(this), + cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java b/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java new file mode 100644 index 000000000..a1872d2f2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.entities; + +public interface ListItem extends Comparable { + public String getDisplayName(); + + public String getJid(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Message.java b/conversations/src/main/java/eu/siacs/conversations/entities/Message.java new file mode 100644 index 000000000..a390c7ca0 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Message.java @@ -0,0 +1,478 @@ +package eu.siacs.conversations.entities; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +public class Message extends AbstractEntity { + + public static final String TABLENAME = "messages"; + + public static final int STATUS_RECEIVED = 0; + public static final int STATUS_UNSEND = 1; + public static final int STATUS_SEND = 2; + public static final int STATUS_SEND_FAILED = 3; + public static final int STATUS_SEND_REJECTED = 4; + public static final int STATUS_WAITING = 5; + public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; + + public static final int ENCRYPTION_NONE = 0; + public static final int ENCRYPTION_PGP = 1; + public static final int ENCRYPTION_OTR = 2; + public static final int ENCRYPTION_DECRYPTED = 3; + public static final int ENCRYPTION_DECRYPTION_FAILED = 4; + + public static final int TYPE_TEXT = 0; + public static final int TYPE_IMAGE = 1; + public static final int TYPE_AUDIO = 2; + public static final int TYPE_STATUS = 3; + public static final int TYPE_PRIVATE = 4; + + public static String CONVERSATION = "conversationUuid"; + public static String COUNTERPART = "counterpart"; + public static String TRUE_COUNTERPART = "trueCounterpart"; + public static String BODY = "body"; + public static String TIME_SENT = "timeSent"; + public static String ENCRYPTION = "encryption"; + public static String STATUS = "status"; + public static String TYPE = "type"; + public static String REMOTE_MSG_ID = "remoteMsgId"; + + protected String conversationUuid; + protected String counterpart; + protected String trueCounterpart; + protected String body; + protected String encryptedBody; + protected long timeSent; + protected int encryption; + protected int status; + protected int type; + protected boolean read = true; + protected String remoteMsgId = null; + + protected Conversation conversation = null; + protected Downloadable downloadable = null; + public boolean markable = false; + + private Message mNextMessage = null; + private Message mPreviousMessage = null; + + private Message() { + + } + + public Message(Conversation conversation, String body, int encryption) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), + conversation.getContactJid(), null, body, System + .currentTimeMillis(), encryption, + Message.STATUS_UNSEND, TYPE_TEXT, null); + this.conversation = conversation; + } + + public Message(Conversation conversation, String counterpart, String body, + int encryption, int status) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), + counterpart, null, body, System.currentTimeMillis(), + encryption, status, TYPE_TEXT, null); + this.conversation = conversation; + } + + public Message(String uuid, String conversationUUid, String counterpart, + String trueCounterpart, String body, long timeSent, int encryption, + int status, int type, String remoteMsgId) { + this.uuid = uuid; + this.conversationUuid = conversationUUid; + this.counterpart = counterpart; + this.trueCounterpart = trueCounterpart; + this.body = body; + this.timeSent = timeSent; + this.encryption = encryption; + this.status = status; + this.type = type; + this.remoteMsgId = remoteMsgId; + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(CONVERSATION, conversationUuid); + values.put(COUNTERPART, counterpart); + values.put(TRUE_COUNTERPART, trueCounterpart); + values.put(BODY, body); + values.put(TIME_SENT, timeSent); + values.put(ENCRYPTION, encryption); + values.put(STATUS, status); + values.put(TYPE, type); + values.put(REMOTE_MSG_ID, remoteMsgId); + return values; + } + + public String getConversationUuid() { + return conversationUuid; + } + + public Conversation getConversation() { + return this.conversation; + } + + public String getCounterpart() { + return counterpart; + } + + public Contact getContact() { + if (this.conversation.getMode() == Conversation.MODE_SINGLE) { + return this.conversation.getContact(); + } else { + if (this.trueCounterpart == null) { + return null; + } else { + return this.conversation.getAccount().getRoster() + .getContactFromRoster(this.trueCounterpart); + } + } + } + + public String getBody() { + return body; + } + + public String getReadableBody(Context context) { + if (encryption == ENCRYPTION_PGP) { + return context.getText(R.string.encrypted_message_received) + .toString(); + } else if (encryption == ENCRYPTION_DECRYPTION_FAILED) { + return context.getText(R.string.decryption_failed).toString(); + } else if (type == TYPE_IMAGE) { + return context.getText(R.string.image_file).toString(); + } else { + return body.trim(); + } + } + + public long getTimeSent() { + return timeSent; + } + + public int getEncryption() { + return encryption; + } + + public int getStatus() { + return status; + } + + public String getRemoteMsgId() { + return this.remoteMsgId; + } + + public void setRemoteMsgId(String id) { + this.remoteMsgId = id; + } + + public static Message fromCursor(Cursor cursor) { + return new Message(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + cursor.getString(cursor.getColumnIndex(COUNTERPART)), + cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)), + cursor.getString(cursor.getColumnIndex(BODY)), + cursor.getLong(cursor.getColumnIndex(TIME_SENT)), + cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID))); + } + + public void setConversation(Conversation conv) { + this.conversation = conv; + } + + public void setStatus(int status) { + this.status = status; + } + + public boolean isRead() { + return this.read; + } + + public void markRead() { + this.read = true; + } + + public void markUnread() { + this.read = false; + } + + public void setTime(long time) { + this.timeSent = time; + } + + public void setEncryption(int encryption) { + this.encryption = encryption; + } + + public void setBody(String body) { + this.body = body; + } + + public String getEncryptedBody() { + return this.encryptedBody; + } + + public void setEncryptedBody(String body) { + this.encryptedBody = body; + } + + public void setType(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public void setPresence(String presence) { + if (presence == null) { + this.counterpart = this.counterpart.split("/", 2)[0]; + } else { + this.counterpart = this.counterpart.split("/", 2)[0] + "/" + + presence; + } + } + + public void setTrueCounterpart(String trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + + public String getPresence() { + String[] counterparts = this.counterpart.split("/", 2); + if (counterparts.length == 2) { + return counterparts[1]; + } else { + if (this.counterpart.contains("/")) { + return ""; + } else { + return null; + } + } + } + + public void setDownloadable(Downloadable downloadable) { + this.downloadable = downloadable; + } + + public Downloadable getDownloadable() { + return this.downloadable; + } + + public static Message createStatusMessage(Conversation conversation) { + Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + return message; + } + + public void setCounterpart(String counterpart) { + this.counterpart = counterpart; + } + + public boolean equals(Message message) { + if ((this.remoteMsgId != null) && (this.body != null) + && (this.counterpart != null)) { + return this.remoteMsgId.equals(message.getRemoteMsgId()) + && this.body.equals(message.getBody()) + && this.counterpart.equals(message.getCounterpart()); + } else { + return false; + } + } + + public Message next() { + if (this.mNextMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index < 0 + || index >= this.conversation.getMessages().size() - 1) { + this.mNextMessage = null; + } else { + this.mNextMessage = this.conversation.messages + .get(index + 1); + } + } + } + return this.mNextMessage; + } + + public Message prev() { + if (this.mPreviousMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index <= 0 || index > this.conversation.messages.size()) { + this.mPreviousMessage = null; + } else { + this.mPreviousMessage = this.conversation.messages + .get(index - 1); + } + } + } + return this.mPreviousMessage; + } + + public boolean mergable(Message message) { + if (message == null) { + return false; + } + return (message.getType() == Message.TYPE_TEXT + && this.getDownloadable() == null + && message.getDownloadable() == null + && message.getEncryption() != Message.ENCRYPTION_PGP + && this.getType() == message.getType() + && this.getEncryption() == message.getEncryption() + && this.getCounterpart().equals(message.getCounterpart()) + && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && ((this + .getStatus() == message.getStatus() || ((this.getStatus() == Message.STATUS_SEND || this + .getStatus() == Message.STATUS_SEND_RECEIVED) && (message + .getStatus() == Message.STATUS_UNSEND + || message.getStatus() == Message.STATUS_SEND || message + .getStatus() == Message.STATUS_SEND_DISPLAYED))))); + } + + public String getMergedBody() { + Message next = this.next(); + if (this.mergable(next)) { + return body.trim() + '\n' + next.getMergedBody(); + } + return body.trim(); + } + + public int getMergedStatus() { + Message next = this.next(); + if (this.mergable(next)) { + return next.getMergedStatus(); + } else { + return getStatus(); + } + } + + public long getMergedTimeSent() { + Message next = this.next(); + if (this.mergable(next)) { + return next.getMergedTimeSent(); + } else { + return getTimeSent(); + } + } + + public boolean wasMergedIntoPrevious() { + Message prev = this.prev(); + if (prev == null) { + return false; + } else { + return prev.mergable(this); + } + } + + public boolean bodyContainsDownloadable() { + Contact contact = this.getContact(); + if (status <= STATUS_RECEIVED + && (contact == null || !contact.trusted())) { + return false; + } + try { + URL url = new URL(this.getBody()); + if (!url.getProtocol().equalsIgnoreCase("http") + && !url.getProtocol().equalsIgnoreCase("https")) { + return false; + } + if (url.getPath() == null) { + return false; + } + String[] pathParts = url.getPath().split("/"); + String filename = pathParts[pathParts.length - 1]; + String[] extensionParts = filename.split("\\."); + if (extensionParts.length == 2 + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 1])) { + return true; + } else if (extensionParts.length == 3 + && Arrays + .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) + .contains(extensionParts[extensionParts.length - 1]) + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 2])) { + return true; + } else { + return false; + } + } catch (MalformedURLException e) { + return false; + } + } + + public ImageParams getImageParams() { + ImageParams params = new ImageParams(); + if (this.downloadable != null) { + params.size = this.downloadable.getFileSize(); + } + if (body == null) { + return params; + } + String parts[] = body.split(","); + if (parts.length == 1) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.origin = parts[0]; + } + } else if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.height = 0; + } + } else if (parts.length == 4) { + params.origin = parts[0]; + try { + params.size = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + params.height = 0; + } + } + return params; + } + + public class ImageParams { + public long size = 0; + public int width = 0; + public int height = 0; + public String origin; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java new file mode 100644 index 000000000..d7407cd5e --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -0,0 +1,369 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; + +@SuppressLint("DefaultLocale") +public class MucOptions { + public static final int ERROR_NO_ERROR = 0; + public static final int ERROR_NICK_IN_USE = 1; + public static final int ERROR_ROOM_NOT_FOUND = 2; + public static final int ERROR_PASSWORD_REQUIRED = 3; + public static final int ERROR_BANNED = 4; + public static final int ERROR_MEMBERS_ONLY = 5; + + public static final int KICKED_FROM_ROOM = 9; + + public static final String STATUS_CODE_BANNED = "301"; + public static final String STATUS_CODE_KICKED = "307"; + + public interface OnRenameListener { + public void onRename(boolean success); + } + + public class User { + public static final int ROLE_MODERATOR = 3; + public static final int ROLE_NONE = 0; + public static final int ROLE_PARTICIPANT = 2; + public static final int ROLE_VISITOR = 1; + public static final int AFFILIATION_ADMIN = 4; + public static final int AFFILIATION_OWNER = 3; + public static final int AFFILIATION_MEMBER = 2; + public static final int AFFILIATION_OUTCAST = 1; + public static final int AFFILIATION_NONE = 0; + + private int role; + private int affiliation; + private String name; + private String jid; + private long pgpKeyId = 0; + + public String getName() { + return name; + } + + public void setName(String user) { + this.name = user; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public int getRole() { + return this.role; + } + + public void setRole(String role) { + role = role.toLowerCase(); + if (role.equals("moderator")) { + this.role = ROLE_MODERATOR; + } else if (role.equals("participant")) { + this.role = ROLE_PARTICIPANT; + } else if (role.equals("visitor")) { + this.role = ROLE_VISITOR; + } else { + this.role = ROLE_NONE; + } + } + + public int getAffiliation() { + return this.affiliation; + } + + public void setAffiliation(String affiliation) { + if (affiliation.equalsIgnoreCase("admin")) { + this.affiliation = AFFILIATION_ADMIN; + } else if (affiliation.equalsIgnoreCase("owner")) { + this.affiliation = AFFILIATION_OWNER; + } else if (affiliation.equalsIgnoreCase("member")) { + this.affiliation = AFFILIATION_MEMBER; + } else if (affiliation.equalsIgnoreCase("outcast")) { + this.affiliation = AFFILIATION_OUTCAST; + } else { + this.affiliation = AFFILIATION_NONE; + } + } + + public void setPgpKeyId(long id) { + this.pgpKeyId = id; + } + + public long getPgpKeyId() { + return this.pgpKeyId; + } + + public Contact getContact() { + return account.getRoster().getContactFromRoster(getJid()); + } + } + + private Account account; + private List users = new CopyOnWriteArrayList(); + private Conversation conversation; + private boolean isOnline = false; + private int error = ERROR_ROOM_NOT_FOUND; + private OnRenameListener renameListener = null; + private boolean aboutToRename = false; + private User self = new User(); + private String subject = null; + private String joinnick; + private String password = null; + + public MucOptions(Conversation conversation) { + this.account = conversation.getAccount(); + this.conversation = conversation; + } + + public void deleteUser(String name) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(name)) { + users.remove(i); + return; + } + } + } + + public void addUser(User user) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(user.getName())) { + users.set(i, user); + return; + } + } + users.add(user); + } + + public void processPacket(PresencePacket packet, PgpEngine pgp) { + String[] fromParts = packet.getFrom().split("/", 2); + if (fromParts.length >= 2) { + String name = fromParts[1]; + String type = packet.getAttribute("type"); + if (type == null) { + User user = new User(); + Element item = packet.findChild("x", + "http://jabber.org/protocol/muc#user") + .findChild("item"); + user.setName(name); + user.setAffiliation(item.getAttribute("affiliation")); + user.setRole(item.getAttribute("role")); + user.setJid(item.getAttribute("jid")); + user.setName(name); + if (name.equals(this.joinnick)) { + this.isOnline = true; + this.error = ERROR_NO_ERROR; + self = user; + if (aboutToRename) { + if (renameListener != null) { + renameListener.onRename(true); + } + aboutToRename = false; + } + } else { + addUser(user); + } + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + user.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + } else if (type.equals("unavailable") && name.equals(this.joinnick)) { + Element x = packet.findChild("x", + "http://jabber.org/protocol/muc#user"); + if (x != null) { + Element status = x.findChild("status"); + if (status != null) { + String code = status.getAttribute("code"); + if (STATUS_CODE_KICKED.equals(code)) { + this.isOnline = false; + this.error = KICKED_FROM_ROOM; + } else if (STATUS_CODE_BANNED.equals(code)) { + this.isOnline = false; + this.error = ERROR_BANNED; + } + } + } + } else if (type.equals("unavailable")) { + deleteUser(packet.getAttribute("from").split("/", 2)[1]); + } else if (type.equals("error")) { + Element error = packet.findChild("error"); + if (error != null && error.hasChild("conflict")) { + if (aboutToRename) { + if (renameListener != null) { + renameListener.onRename(false); + } + aboutToRename = false; + this.setJoinNick(getActualNick()); + } else { + this.error = ERROR_NICK_IN_USE; + } + } else if (error != null && error.hasChild("not-authorized")) { + this.error = ERROR_PASSWORD_REQUIRED; + } else if (error != null && error.hasChild("forbidden")) { + this.error = ERROR_BANNED; + } else if (error != null + && error.hasChild("registration-required")) { + this.error = ERROR_MEMBERS_ONLY; + } + } + } + } + + public List getUsers() { + return this.users; + } + + public String getProposedNick() { + String[] mucParts = conversation.getContactJid().split("/", 2); + if (conversation.getBookmark() != null + && conversation.getBookmark().getNick() != null) { + return conversation.getBookmark().getNick(); + } else { + if (mucParts.length == 2) { + return mucParts[1]; + } else { + return account.getUsername(); + } + } + } + + public String getActualNick() { + if (this.self.getName() != null) { + return this.self.getName(); + } else { + return this.getProposedNick(); + } + } + + public void setJoinNick(String nick) { + this.joinnick = nick; + } + + public boolean online() { + return this.isOnline; + } + + public int getError() { + return this.error; + } + + public void setOnRenameListener(OnRenameListener listener) { + this.renameListener = listener; + } + + public OnRenameListener getOnRenameListener() { + return this.renameListener; + } + + public void setOffline() { + this.users.clear(); + this.error = 0; + this.isOnline = false; + } + + public User getSelf() { + return self; + } + + public void setSubject(String content) { + this.subject = content; + } + + public String getSubject() { + return this.subject; + } + + public void flagAboutToRename() { + this.aboutToRename = true; + } + + public long[] getPgpKeyIds() { + List ids = new ArrayList(); + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { + ids.add(user.getPgpKeyId()); + } + } + long[] primitivLongArray = new long[ids.size()]; + for (int i = 0; i < ids.size(); ++i) { + primitivLongArray[i] = ids.get(i); + } + return primitivLongArray; + } + + public boolean pgpKeysInUse() { + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { + return true; + } + } + return false; + } + + public boolean everybodyHasKeys() { + for (User user : getUsers()) { + if (user.getPgpKeyId() == 0) { + return false; + } + } + return true; + } + + public String getJoinJid() { + return this.conversation.getContactJid().split("/", 2)[0] + "/" + + this.joinnick; + } + + public String getTrueCounterpart(String counterpart) { + for (User user : this.getUsers()) { + if (user.getName().equals(counterpart)) { + return user.getJid(); + } + } + return null; + } + + public String getPassword() { + this.password = conversation + .getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD); + if (this.password == null && conversation.getBookmark() != null + && conversation.getBookmark().getPassword() != null) { + return conversation.getBookmark().getPassword(); + } else { + return this.password; + } + } + + public void setPassword(String password) { + if (conversation.getBookmark() != null) { + conversation.getBookmark().setPassword(password); + } else { + this.password = password; + } + conversation + .setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password); + } + + public Conversation getConversation() { + return this.conversation; + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java b/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java new file mode 100644 index 000000000..b58998473 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.entities; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; + +import eu.siacs.conversations.xml.Element; + +public class Presences { + + public static final int CHAT = -1; + public static final int ONLINE = 0; + public static final int AWAY = 1; + public static final int XA = 2; + public static final int DND = 3; + public static final int OFFLINE = 4; + + private Hashtable presences = new Hashtable(); + + public Hashtable getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + this.presences.put(resource, status); + } + + public void removePresence(String resource) { + this.presences.remove(resource); + } + + public void clearPresences() { + this.presences.clear(); + } + + public int getMostAvailableStatus() { + int status = OFFLINE; + Iterator> it = presences.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + if (entry.getValue() < status) + status = entry.getValue(); + } + return status; + } + + public static int parseShow(Element show) { + if ((show == null) || (show.getContent() == null)) { + return Presences.ONLINE; + } else if (show.getContent().equals("away")) { + return Presences.AWAY; + } else if (show.getContent().equals("xa")) { + return Presences.XA; + } else if (show.getContent().equals("chat")) { + return Presences.CHAT; + } else if (show.getContent().equals("dnd")) { + return Presences.DND; + } else { + return Presences.OFFLINE; + } + } + + public int size() { + return presences.size(); + } + + public String[] asStringArray() { + final String[] presencesArray = new String[presences.size()]; + presences.keySet().toArray(presencesArray); + return presencesArray; + } + + public boolean has(String presence) { + return presences.containsKey(presence); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java b/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java new file mode 100644 index 000000000..3267b15ae --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -0,0 +1,83 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +public class Roster { + Account account; + ConcurrentHashMap contacts = new ConcurrentHashMap(); + private String version = null; + + public Roster(Account account) { + this.account = account; + } + + public Contact getContactFromRoster(String jid) { + if (jid == null) { + return null; + } + String cleanJid = jid.split("/", 2)[0]; + Contact contact = contacts.get(cleanJid); + if (contact != null && contact.showInRoster()) { + return contact; + } else { + return null; + } + } + + public Contact getContact(String jid) { + String cleanJid = jid.split("/", 2)[0].toLowerCase(Locale.getDefault()); + if (contacts.containsKey(cleanJid)) { + return contacts.get(cleanJid); + } else { + Contact contact = new Contact(cleanJid); + contact.setAccount(account); + contacts.put(cleanJid, contact); + return contact; + } + } + + public void clearPresences() { + for (Contact contact : getContacts()) { + contact.clearPresences(); + } + } + + public void markAllAsNotInRoster() { + for (Contact contact : getContacts()) { + contact.resetOption(Contact.Options.IN_ROSTER); + } + } + + public void clearSystemAccounts() { + for (Contact contact : getContacts()) { + contact.setPhotoUri(null); + contact.setSystemName(null); + contact.setSystemAccount(null); + } + } + + public List getContacts() { + return new ArrayList(this.contacts.values()); + } + + public void initContact(Contact contact) { + contact.setAccount(account); + contact.setOption(Contact.Options.IN_ROSTER); + contacts.put(contact.getJid(), contact); + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + + public Account getAccount() { + return this.account; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java new file mode 100644 index 000000000..c96d116d0 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -0,0 +1,48 @@ +package eu.siacs.conversations.generator; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import eu.siacs.conversations.services.XmppConnectionService; + +import android.util.Base64; + +public abstract class AbstractGenerator { + public final String[] FEATURES = { "urn:xmpp:jingle:1", + "urn:xmpp:jingle:apps:file-transfer:3", + "urn:xmpp:jingle:transports:s5b:1", + "urn:xmpp:jingle:transports:ibb:1", "urn:xmpp:receipts", + "urn:xmpp:chat-markers:0", "http://jabber.org/protocol/muc", + "jabber:x:conference", "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify" }; + public final String IDENTITY_NAME = "Conversations 0.7"; + public final String IDENTITY_TYPE = "phone"; + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractGenerator(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public String getCapHash() { + StringBuilder s = new StringBuilder(); + s.append("client/" + IDENTITY_TYPE + "//" + IDENTITY_NAME + "<"); + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + List features = Arrays.asList(FEATURES); + Collections.sort(features); + for (String feature : features) { + s.append(feature + "<"); + } + byte[] sha1 = md.digest(s.toString().getBytes()); + return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java new file mode 100644 index 000000000..d44bf0ca1 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -0,0 +1,96 @@ +package eu.siacs.conversations.generator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class IqGenerator extends AbstractGenerator { + + public IqGenerator(XmppConnectionService service) { + super(service); + } + + public IqPacket discoResponse(IqPacket request) { + IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT); + packet.setId(request.getId()); + packet.setTo(request.getFrom()); + Element query = packet.addChild("query", + "http://jabber.org/protocol/disco#info"); + query.setAttribute("node", request.query().getAttribute("node")); + Element identity = query.addChild("identity"); + identity.setAttribute("category", "client"); + identity.setAttribute("type", this.IDENTITY_TYPE); + identity.setAttribute("name", IDENTITY_NAME); + List features = Arrays.asList(FEATURES); + Collections.sort(features); + for (String feature : features) { + query.addChild("feature").setAttribute("var", feature); + } + return packet; + } + + protected IqPacket publish(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + Element publish = pubsub.addChild("publish"); + publish.setAttribute("node", node); + publish.addChild(item); + return packet; + } + + protected IqPacket retrieve(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_GET); + Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + Element items = pubsub.addChild("items"); + items.setAttribute("node", node); + if (item != null) { + items.addChild(item); + } + return packet; + } + + public IqPacket publishAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element data = item.addChild("data", "urn:xmpp:avatar:data"); + data.setContent(avatar.image); + return publish("urn:xmpp:avatar:data", item); + } + + public IqPacket publishAvatarMetadata(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element metadata = item + .addChild("metadata", "urn:xmpp:avatar:metadata"); + Element info = metadata.addChild("info"); + info.setAttribute("bytes", avatar.size); + info.setAttribute("id", avatar.sha1sum); + info.setAttribute("height", avatar.height); + info.setAttribute("width", avatar.height); + info.setAttribute("type", avatar.type); + return publish("urn:xmpp:avatar:metadata", item); + } + + public IqPacket retrieveAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + packet.setTo(avatar.owner); + return packet; + } + + public IqPacket retrieveAvatarMetaData(String to) { + IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + if (to != null) { + packet.setTo(to); + } + return packet; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java new file mode 100644 index 000000000..dd833e56c --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -0,0 +1,178 @@ +package eu.siacs.conversations.generator; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageGenerator extends AbstractGenerator { + public MessageGenerator(XmppConnectionService service) { + super(service); + } + + private MessagePacket preparePacket(Message message, boolean addDelay) { + Conversation conversation = message.getConversation(); + Account account = conversation.getAccount(); + MessagePacket packet = new MessagePacket(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + packet.addChild("markable", "urn:xmpp:chat-markers:0"); + if (this.mXmppConnectionService.indicateReceived()) { + packet.addChild("request", "urn:xmpp:receipts"); + } + } else if (message.getType() == Message.TYPE_PRIVATE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + } else { + packet.setTo(message.getCounterpart().split("/", 2)[0]); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + } + packet.setFrom(account.getFullJid()); + packet.setId(message.getUuid()); + if (addDelay) { + addDelay(packet, message.getTimeSent()); + } + return packet; + } + + private void addDelay(MessagePacket packet, long timestamp) { + final SimpleDateFormat mDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + Element delay = packet.addChild("delay", "urn:xmpp:delay"); + Date date = new Date(timestamp); + delay.setAttribute("stamp", mDateFormat.format(date)); + } + + public MessagePacket generateOtrChat(Message message) { + return generateOtrChat(message, false); + } + + public MessagePacket generateOtrChat(Message message, boolean addDelay) { + Session otrSession = message.getConversation().getOtrSession(); + if (otrSession == null) { + return null; + } + MessagePacket packet = preparePacket(message, addDelay); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + try { + packet.setBody(otrSession.transformSending(message.getBody())); + return packet; + } catch (OtrException e) { + return null; + } + } + + public MessagePacket generateChat(Message message) { + return generateChat(message, false); + } + + public MessagePacket generateChat(Message message, boolean addDelay) { + MessagePacket packet = preparePacket(message, addDelay); + packet.setBody(message.getBody()); + return packet; + } + + public MessagePacket generatePgpChat(Message message) { + return generatePgpChat(message, false); + } + + public MessagePacket generatePgpChat(Message message, boolean addDelay) { + MessagePacket packet = preparePacket(message, addDelay); + packet.setBody("This is an XEP-0027 encryted message"); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getEncryptedBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getBody()); + } + return packet; + } + + public MessagePacket generateNotAcceptable(MessagePacket origin) { + MessagePacket packet = generateError(origin); + Element error = packet.addChild("error"); + error.setAttribute("type", "modify"); + error.setAttribute("code", "406"); + error.addChild("not-acceptable"); + return packet; + } + + private MessagePacket generateError(MessagePacket origin) { + MessagePacket packet = new MessagePacket(); + packet.setId(origin.getId()); + packet.setTo(origin.getFrom()); + packet.setBody(origin.getBody()); + packet.setType(MessagePacket.TYPE_ERROR); + return packet; + } + + public MessagePacket confirm(Account account, String to, String id) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(to); + packet.setFrom(account.getFullJid()); + Element received = packet.addChild("displayed", + "urn:xmpp:chat-markers:0"); + received.setAttribute("id", id); + return packet; + } + + public MessagePacket conferenceSubject(Conversation conversation, + String subject) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + packet.setTo(conversation.getContactJid().split("/", 2)[0]); + Element subjectChild = new Element("subject"); + subjectChild.setContent(subject); + packet.addChild(subjectChild); + packet.setFrom(conversation.getAccount().getJid()); + return packet; + } + + public MessagePacket directInvite(Conversation conversation, String contact) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(contact); + packet.setFrom(conversation.getAccount().getFullJid()); + Element x = packet.addChild("x", "jabber:x:conference"); + x.setAttribute("jid", conversation.getContactJid().split("/", 2)[0]); + return packet; + } + + public MessagePacket invite(Conversation conversation, String contact) { + MessagePacket packet = new MessagePacket(); + packet.setTo(conversation.getContactJid().split("/", 2)[0]); + packet.setFrom(conversation.getAccount().getFullJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); + Element invite = new Element("invite"); + invite.setAttribute("to", contact); + x.addChild(invite); + packet.addChild(x); + return packet; + } + + public MessagePacket received(Account account, + MessagePacket originalMessage, String namespace) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setTo(originalMessage.getFrom()); + receivedPacket.setFrom(account.getFullJid()); + Element received = receivedPacket.addChild("received", namespace); + received.setAttribute("id", originalMessage.getId()); + return receivedPacket; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java new file mode 100644 index 000000000..d896dd001 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -0,0 +1,57 @@ +package eu.siacs.conversations.generator; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceGenerator extends AbstractGenerator { + + public PresenceGenerator(XmppConnectionService service) { + super(service); + } + + private PresencePacket subscription(String type, Contact contact) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("type", type); + packet.setAttribute("to", contact.getJid()); + packet.setAttribute("from", contact.getAccount().getJid()); + return packet; + } + + public PresencePacket requestPresenceUpdatesFrom(Contact contact) { + return subscription("subscribe", contact); + } + + public PresencePacket stopPresenceUpdatesFrom(Contact contact) { + return subscription("unsubscribe", contact); + } + + public PresencePacket stopPresenceUpdatesTo(Contact contact) { + return subscription("unsubscribed", contact); + } + + public PresencePacket sendPresenceUpdatesTo(Contact contact) { + return subscription("subscribed", contact); + } + + public PresencePacket sendPresence(Account account) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("from", account.getFullJid()); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + String capHash = getCapHash(); + if (capHash != null) { + Element cap = packet.addChild("c", + "http://jabber.org/protocol/caps"); + cap.setAttribute("hash", "sha-1"); + cap.setAttribute("node", "http://conversions.siacs.eu"); + cap.setAttribute("ver", capHash); + } + return packet; + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java new file mode 100644 index 000000000..407a13d94 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java @@ -0,0 +1,255 @@ +package eu.siacs.conversations.http; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; + +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; + +public class HttpConnection implements Downloadable { + + private HttpConnectionManager mHttpConnectionManager; + private XmppConnectionService mXmppConnectionService; + + private URL mUrl; + private Message message; + private DownloadableFile file; + private int mStatus = Downloadable.STATUS_UNKNOWN; + + public HttpConnection(HttpConnectionManager manager) { + this.mHttpConnectionManager = manager; + this.mXmppConnectionService = manager.getXmppConnectionService(); + } + + @Override + public boolean start() { + if (mXmppConnectionService.hasInternetConnection()) { + if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { + checkFileSize(true); + } else { + new Thread(new FileDownloader(true)).start(); + } + return true; + } else { + return false; + } + } + + public void init(Message message) { + this.message = message; + this.message.setDownloadable(this); + try { + mUrl = new URL(message.getBody()); + this.file = mXmppConnectionService.getFileBackend().getFile( + message, false); + String reference = mUrl.getRef(); + if (reference != null && reference.length() == 96) { + this.file.setKey(CryptoHelper.hexToBytes(reference)); + } + if (this.message.getEncryption() == Message.ENCRYPTION_OTR + && this.file.getKey() == null) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + checkFileSize(false); + } catch (MalformedURLException e) { + this.cancel(); + } + } + + private void checkFileSize(boolean interactive) { + new Thread(new FileSizeChecker(interactive)).start(); + } + + public void cancel() { + mHttpConnectionManager.finishConnection(this); + message.setDownloadable(null); + mXmppConnectionService.updateConversationUi(); + } + + private void finish() { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + message.setDownloadable(null); + mHttpConnectionManager.finishConnection(this); + } + + private void changeStatus(int status) { + this.mStatus = status; + mXmppConnectionService.updateConversationUi(); + } + + private void setupTrustManager(HttpsURLConnection connection, + boolean interactive) { + X509TrustManager trustManager; + HostnameVerifier hostnameVerifier; + if (interactive) { + trustManager = mXmppConnectionService.getMemorizingTrustManager(); + hostnameVerifier = mXmppConnectionService + .getMemorizingTrustManager().wrapHostnameVerifier( + new StrictHostnameVerifier()); + } else { + trustManager = mXmppConnectionService.getMemorizingTrustManager() + .getNonInteractive(); + hostnameVerifier = mXmppConnectionService + .getMemorizingTrustManager() + .wrapHostnameVerifierNonInteractive( + new StrictHostnameVerifier()); + } + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, new X509TrustManager[] { trustManager }, + mXmppConnectionService.getRNG()); + connection.setSSLSocketFactory(sc.getSocketFactory()); + connection.setHostnameVerifier(hostnameVerifier); + } catch (KeyManagementException e) { + return; + } catch (NoSuchAlgorithmException e) { + return; + } + } + + private class FileSizeChecker implements Runnable { + + private boolean interactive = false; + + public FileSizeChecker(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + long size; + try { + size = retrieveFileSize(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER_CHECK_FILESIZE); + return; + } catch (IOException e) { + cancel(); + return; + } + file.setExpectedSize(size); + if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) { + new Thread(new FileDownloader(interactive)).start(); + } else { + changeStatus(STATUS_OFFER); + } + } + + private long retrieveFileSize() throws IOException, + SSLHandshakeException { + changeStatus(STATUS_CHECKING); + HttpURLConnection connection = (HttpURLConnection) mUrl + .openConnection(); + connection.setRequestMethod("HEAD"); + if (connection instanceof HttpsURLConnection) { + setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + String contentLength = connection.getHeaderField("Content-Length"); + if (contentLength == null) { + throw new IOException(); + } + try { + return Long.parseLong(contentLength, 10); + } catch (NumberFormatException e) { + throw new IOException(); + } + } + + } + + private class FileDownloader implements Runnable { + + private boolean interactive = false; + + public FileDownloader(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + try { + changeStatus(STATUS_DOWNLOADING); + download(); + updateImageBounds(); + finish(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER); + } catch (IOException e) { + cancel(); + } + } + + private void download() throws SSLHandshakeException, IOException { + HttpURLConnection connection = (HttpURLConnection) mUrl + .openConnection(); + if (connection instanceof HttpsURLConnection) { + setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + BufferedInputStream is = new BufferedInputStream( + connection.getInputStream()); + OutputStream os = file.createOutputStream(); + int count = -1; + byte[] buffer = new byte[1024]; + while ((count = is.read(buffer)) != -1) { + os.write(buffer, 0, count); + } + os.flush(); + os.close(); + is.close(); + } + + private void updateImageBounds() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(mUrl.toString() + "," + file.getSize() + ',' + + imageWidth + ',' + imageHeight); + message.setType(Message.TYPE_IMAGE); + mXmppConnectionService.updateMessage(message); + } + + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java new file mode 100644 index 000000000..9a2a24052 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -0,0 +1,28 @@ +package eu.siacs.conversations.http; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; + +public class HttpConnectionManager extends AbstractConnectionManager { + + public HttpConnectionManager(XmppConnectionService service) { + super(service); + } + + private List connections = new CopyOnWriteArrayList(); + + public HttpConnection createNewConnection(Message message) { + HttpConnection connection = new HttpConnection(this); + connection.init(message); + this.connections.add(connection); + return connection; + } + + public void finishConnection(HttpConnection connection) { + this.connections.remove(connection); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java new file mode 100644 index 000000000..5541c1c61 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -0,0 +1,92 @@ +package eu.siacs.conversations.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; + +public abstract class AbstractParser { + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractParser(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + protected long getTimestamp(Element packet) { + long now = System.currentTimeMillis(); + ArrayList stamps = new ArrayList(); + for (Element child : packet.getChildren()) { + if (child.getName().equals("delay")) { + stamps.add(child.getAttribute("stamp").replace("Z", "+0000")); + } + } + Collections.sort(stamps); + if (stamps.size() >= 1) { + try { + String stamp = stamps.get(stamps.size() - 1); + if (stamp.contains(".")) { + Date date = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + .parse(stamp); + if (now < date.getTime()) { + return now; + } else { + return date.getTime(); + } + } else { + Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", + Locale.US).parse(stamp); + if (now < date.getTime()) { + return now; + } else { + return date.getTime(); + } + } + } catch (ParseException e) { + return now; + } + } else { + return now; + } + } + + protected void updateLastseen(Element packet, Account account, + boolean presenceOverwrite) { + String[] fromParts = packet.getAttribute("from").split("/", 2); + String from = fromParts[0]; + String presence = null; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + Contact contact = account.getRoster().getContact(from); + long timestamp = getTimestamp(packet); + if (timestamp >= contact.lastseen.time) { + contact.lastseen.time = timestamp; + if ((presence != null) && (presenceOverwrite)) { + contact.lastseen.presence = presence; + } + } + } + + protected String avatarData(Element items) { + Element item = items.findChild("item"); + if (item == null) { + return null; + } + Element data = item.findChild("data", "urn:xmpp:avatar:data"); + if (data == null) { + return null; + } + return data.getContent(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java new file mode 100644 index 000000000..df6754f26 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -0,0 +1,92 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class IqParser extends AbstractParser implements OnIqPacketReceived { + + public IqParser(XmppConnectionService service) { + super(service); + } + + public void rosterItems(Account account, Element query) { + String version = query.getAttribute("ver"); + if (version != null) { + account.getRoster().setVersion(version); + } + for (Element item : query.getChildren()) { + if (item.getName().equals("item")) { + String jid = item.getAttribute("jid"); + String name = item.getAttribute("name"); + String subscription = item.getAttribute("subscription"); + Contact contact = account.getRoster().getContact(jid); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + } + if (subscription != null) { + if (subscription.equals("remove")) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else { + contact.setOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.parseSubscriptionFromElement(item); + } + } + } + } + mXmppConnectionService.updateRosterUi(); + } + + public String avatarData(IqPacket packet) { + Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub == null) { + return null; + } + Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return super.avatarData(items); + } + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.hasChild("query", "jabber:iq:roster")) { + String from = packet.getFrom(); + if ((from == null) || (from.equals(account.getJid()))) { + Element query = packet.findChild("query"); + this.rosterItems(account, query); + } + } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") + || packet.hasChild("data", "http://jabber.org/protocol/ibb")) { + mXmppConnectionService.getJingleConnectionManager() + .deliverIbbPacket(account, packet); + } else if (packet.hasChild("query", + "http://jabber.org/protocol/disco#info")) { + IqPacket response = mXmppConnectionService.getIqGenerator() + .discoResponse(packet); + account.getXmppConnection().sendIqPacket(response, null); + } else if (packet.hasChild("ping", "urn:xmpp:ping")) { + IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } else { + if ((packet.getType() == IqPacket.TYPE_GET) + || (packet.getType() == IqPacket.TYPE_SET)) { + IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("feature-not-implemented", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + account.getXmppConnection().sendIqPacket(response, null); + } + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java new file mode 100644 index 000000000..b5e14305a --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -0,0 +1,517 @@ +package eu.siacs.conversations.parser; + +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.NotificationService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageParser extends AbstractParser implements + OnMessagePacketReceived { + public MessageParser(XmppConnectionService service) { + super(service); + } + + private Message parseChat(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + updateLastseen(packet, account, true); + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, packet.getFrom(), + pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED); + } else { + finishedMessage = new Message(conversation, packet.getFrom(), + packet.getBody(), Message.ENCRYPTION_NONE, + Message.STATUS_RECEIVED); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + if (conversation.getMode() == Conversation.MODE_MULTI + && fromParts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(fromParts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(fromParts[1])); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + private Message parseOtrChat(MessagePacket packet, Account account) { + boolean properlyAddressed = (packet.getTo().split("/", 2).length == 2) + || (account.countPresences() == 1); + String[] fromParts = packet.getFrom().split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + updateLastseen(packet, account, true); + String body = packet.getBody(); + if (body.matches("^\\?OTRv\\d*\\?")) { + conversation.endOtrIfNeeded(); + } + if (!conversation.hasValidOtrSession()) { + if (properlyAddressed) { + conversation.startOtrSession(mXmppConnectionService, presence, + false); + } else { + return null; + } + } else { + String foreignPresence = conversation.getOtrSession() + .getSessionID().getUserID(); + if (!foreignPresence.equals(presence)) { + conversation.endOtrIfNeeded(); + if (properlyAddressed) { + conversation.startOtrSession(mXmppConnectionService, + presence, false); + } else { + return null; + } + } + } + try { + Session otrSession = conversation.getOtrSession(); + SessionStatus before = otrSession.getSessionStatus(); + body = otrSession.transformReceiving(body); + SessionStatus after = otrSession.getSessionStatus(); + if ((before != after) && (after == SessionStatus.ENCRYPTED)) { + mXmppConnectionService.onOtrSessionEstablished(conversation); + } else if ((before != after) && (after == SessionStatus.FINISHED)) { + conversation.resetOtrSession(); + mXmppConnectionService.updateConversationUi(); + } + 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; + } + Message finishedMessage = new Message(conversation, + packet.getFrom(), body, Message.ENCRYPTION_OTR, + Message.STATUS_RECEIVED); + finishedMessage.setTime(getTimestamp(packet)); + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + return finishedMessage; + } catch (Exception e) { + String receivedId = packet.getId(); + if (receivedId != null) { + mXmppConnectionService.replyWithNotAcceptable(account, packet); + } + conversation.resetOtrSession(); + return null; + } + } + + private Message parseGroupchat(MessagePacket packet, Account account) { + int status; + String[] fromParts = packet.getFrom().split("/", 2); + if (mXmppConnectionService.find(account.pendingConferenceLeaves, + account, fromParts[0]) != null) { + return null; + } + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], true); + if (packet.hasChild("subject")) { + conversation.getMucOptions().setSubject( + packet.findChild("subject").getContent()); + mXmppConnectionService.updateConversationUi(); + return null; + } + if ((fromParts.length == 1)) { + return null; + } + String counterPart = fromParts[1]; + if (counterPart.equals(conversation.getMucOptions().getActualNick())) { + if (mXmppConnectionService.markMessage(conversation, + packet.getId(), Message.STATUS_SEND)) { + return null; + } else { + status = Message.STATUS_SEND; + } + } else { + status = Message.STATUS_RECEIVED; + } + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody == null) { + finishedMessage = new Message(conversation, counterPart, + packet.getBody(), Message.ENCRYPTION_NONE, status); + } else { + finishedMessage = new Message(conversation, counterPart, pgpBody, + Message.ENCRYPTION_PGP, status); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + if (status == Message.STATUS_RECEIVED) { + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(counterPart)); + } + if (packet.hasChild("delay") + && conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + private Message parseCarbonMessage(MessagePacket packet, Account account) { + int status; + String fullJid; + Element forwarded; + if (packet.hasChild("received", "urn:xmpp:carbons:2")) { + forwarded = packet.findChild("received", "urn:xmpp:carbons:2") + .findChild("forwarded", "urn:xmpp:forward:0"); + status = Message.STATUS_RECEIVED; + } else if (packet.hasChild("sent", "urn:xmpp:carbons:2")) { + forwarded = packet.findChild("sent", "urn:xmpp:carbons:2") + .findChild("forwarded", "urn:xmpp:forward:0"); + status = Message.STATUS_SEND; + } else { + return null; + } + if (forwarded == null) { + return null; + } + Element message = forwarded.findChild("message"); + if (message == null) { + return null; + } + if (!message.hasChild("body")) { + if (status == Message.STATUS_RECEIVED + && message.getAttribute("from") != null) { + parseNonMessage(message, account); + } else if (status == Message.STATUS_SEND + && message.hasChild("displayed", "urn:xmpp:chat-markers:0")) { + String to = message.getAttribute("to"); + if (to != null) { + Conversation conversation = mXmppConnectionService.find( + mXmppConnectionService.getConversations(), account, + to.split("/")[0]); + if (conversation != null) { + mXmppConnectionService.markRead(conversation, false); + } + } + } + return null; + } + if (status == Message.STATUS_RECEIVED) { + fullJid = message.getAttribute("from"); + if (fullJid == null) { + return null; + } else { + updateLastseen(message, account, true); + } + } else { + fullJid = message.getAttribute("to"); + if (fullJid == null) { + return null; + } + } + String[] parts = fullJid.split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, parts[0], false); + String pgpBody = getPgpBody(message); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, fullJid, pgpBody, + Message.ENCRYPTION_PGP, status); + } else { + String body = message.findChild("body").getContent(); + finishedMessage = new Message(conversation, fullJid, body, + Message.ENCRYPTION_NONE, status); + } + finishedMessage.setTime(getTimestamp(message)); + finishedMessage.setRemoteMsgId(message.getAttribute("id")); + finishedMessage.markable = isMarkable(message); + if (conversation.getMode() == Conversation.MODE_MULTI + && parts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(parts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(parts[1])); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + } + + return finishedMessage; + } + + private void parseError(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/", 2); + mXmppConnectionService.markMessage(account, fromParts[0], + packet.getId(), Message.STATUS_SEND_FAILED); + } + + private void parseNonMessage(Element packet, Account account) { + String from = packet.getAttribute("from"); + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getAttribute("from"), account); + } else if (from != null + && packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) { + String id = packet + .findChild("displayed", "urn:xmpp:chat-markers:0") + .getAttribute("id"); + updateLastseen(packet, account, true); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_DISPLAYED); + } else if (from != null + && packet.hasChild("received", "urn:xmpp:chat-markers:0")) { + String id = packet.findChild("received", "urn:xmpp:chat-markers:0") + .getAttribute("id"); + updateLastseen(packet, account, false); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_RECEIVED); + } else if (from != null + && packet.hasChild("received", "urn:xmpp:receipts")) { + String id = packet.findChild("received", "urn:xmpp:receipts") + .getAttribute("id"); + updateLastseen(packet, account, false); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_RECEIVED); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Element x = packet.findChild("x", + "http://jabber.org/protocol/muc#user"); + if (x.hasChild("invite")) { + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, + packet.getAttribute("from"), true); + if (!conversation.getMucOptions().online()) { + if (x.hasChild("password")) { + Element password = x.findChild("password"); + conversation.getMucOptions().setPassword( + password.getContent()); + mXmppConnectionService.databaseBackend + .updateConversation(conversation); + } + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + } + } else if (packet.hasChild("x", "jabber:x:conference")) { + Element x = packet.findChild("x", "jabber:x:conference"); + String jid = x.getAttribute("jid"); + String password = x.getAttribute("password"); + if (jid != null) { + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, jid, true); + if (!conversation.getMucOptions().online()) { + if (password != null) { + conversation.getMucOptions().setPassword(password); + mXmppConnectionService.databaseBackend + .updateConversation(conversation); + } + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + } + } + } + + private void parseEvent(Element event, String from, Account account) { + Element items = event.findChild("items"); + String node = items.getAttribute("node"); + if (node != null) { + if (node.equals("urn:xmpp:avatar:metadata")) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = from; + if (mXmppConnectionService.getFileBackend().isAvatarCached( + avatar)) { + if (account.getJid().equals(from)) { + if (account.setAvatar(avatar.getFilename())) { + mXmppConnectionService.databaseBackend + .updateAccount(account); + } + mXmppConnectionService.getAvatarService().clear( + account); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateAccountUi(); + } else { + Contact contact = account.getRoster().getContact( + from); + contact.setAvatar(avatar.getFilename()); + mXmppConnectionService.getAvatarService().clear( + contact); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + } + } else { + mXmppConnectionService.fetchAvatar(account, avatar); + } + } + } else if (node.equals("http://jabber.org/protocol/nick")) { + Element item = items.findChild("item"); + if (item != null) { + Element nick = item.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + if (from != null) { + Contact contact = account.getRoster().getContact( + from); + contact.setPresenceName(nick.getContent()); + } + } + } + } + } + } + + private String getPgpBody(Element message) { + Element child = message.findChild("x", "jabber:x:encrypted"); + if (child == null) { + return null; + } else { + return child.getContent(); + } + } + + private boolean isMarkable(Element message) { + return message.hasChild("markable", "urn:xmpp:chat-markers:0"); + } + + @Override + public void onMessagePacketReceived(Account account, MessagePacket packet) { + Message message = null; + boolean notify = mXmppConnectionService.getPreferences().getBoolean( + "show_notification", true); + boolean alwaysNotifyInConference = notify + && mXmppConnectionService.getPreferences().getBoolean( + "always_notify_in_conference", false); + + this.parseNick(packet, account); + + if ((packet.getType() == MessagePacket.TYPE_CHAT || packet.getType() == MessagePacket.TYPE_NORMAL)) { + if ((packet.getBody() != null) + && (packet.getBody().startsWith("?OTR"))) { + message = this.parseOtrChat(packet, account); + if (message != null) { + message.markUnread(); + } + } else if (packet.hasChild("body") + && !(packet.hasChild("x", + "http://jabber.org/protocol/muc#user"))) { + message = this.parseChat(packet, account); + if (message != null) { + message.markUnread(); + } + } else if (packet.hasChild("received", "urn:xmpp:carbons:2") + || (packet.hasChild("sent", "urn:xmpp:carbons:2"))) { + message = this.parseCarbonMessage(packet, account); + if (message != null) { + if (message.getStatus() == Message.STATUS_SEND) { + account.activateGracePeriod(); + notify = false; + mXmppConnectionService.markRead( + message.getConversation(), false); + } else { + message.markUnread(); + } + } + } else { + parseNonMessage(packet, account); + } + } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { + message = this.parseGroupchat(packet, account); + if (message != null) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + message.markUnread(); + notify = alwaysNotifyInConference + || NotificationService + .wasHighlightedOrPrivate(message); + } else { + mXmppConnectionService.markRead(message.getConversation(), + false); + account.activateGracePeriod(); + notify = false; + } + } + } else if (packet.getType() == MessagePacket.TYPE_ERROR) { + this.parseError(packet, account); + return; + } else if (packet.getType() == MessagePacket.TYPE_HEADLINE) { + this.parseHeadline(packet, account); + return; + } + if ((message == null) || (message.getBody() == null)) { + return; + } + if ((mXmppConnectionService.confirmMessages()) + && ((packet.getId() != null))) { + if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:chat-markers:0"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + if (packet.hasChild("request", "urn:xmpp:receipts")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:receipts"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + } + Conversation conversation = message.getConversation(); + conversation.add(message); + if (packet.getType() != MessagePacket.TYPE_ERROR) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || mXmppConnectionService.saveEncryptedMessages()) { + mXmppConnectionService.databaseBackend.createMessage(message); + } + } + if (message.bodyContainsDownloadable()) { + this.mXmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + } + notify = notify && !conversation.isMuted(); + if (notify) { + mXmppConnectionService.getNotificationService().push(message); + } + mXmppConnectionService.updateConversationUi(); + } + + private void parseHeadline(MessagePacket packet, Account account) { + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getFrom(), account); + } + } + + private void parseNick(MessagePacket packet, Account account) { + Element nick = packet.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + if (packet.getFrom() != null) { + Contact contact = account.getRoster().getContact( + packet.getFrom()); + contact.setPresenceName(nick.getContent()); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java new file mode 100644 index 000000000..4e90cda8c --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -0,0 +1,133 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnPresencePacketReceived; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceParser extends AbstractParser implements + OnPresencePacketReceived { + + public PresenceParser(XmppConnectionService service) { + super(service); + } + + public void parseConferencePresence(PresencePacket packet, Account account) { + PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine(); + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Conversation muc = mXmppConnectionService.find(account, packet + .getAttribute("from").split("/", 2)[0]); + if (muc != null) { + boolean before = muc.getMucOptions().online(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before != muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } + mXmppConnectionService.getAvatarService().clear(muc); + } + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + Conversation muc = mXmppConnectionService.find(account, packet + .getAttribute("from").split("/", 2)[0]); + if (muc != null) { + boolean before = muc.getMucOptions().online(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before != muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } + mXmppConnectionService.getAvatarService().clear(muc); + } + } + } + + public void parseContactPresence(PresencePacket packet, Account account) { + PresenceGenerator mPresenceGenerator = mXmppConnectionService + .getPresenceGenerator(); + if (packet.getFrom() == null) { + return; + } + String[] fromParts = packet.getFrom().split("/", 2); + String type = packet.getAttribute("type"); + if (fromParts[0].equals(account.getJid())) { + if (fromParts.length == 2) { + if (type == null) { + account.updatePresence(fromParts[1], + Presences.parseShow(packet.findChild("show"))); + } else if (type.equals("unavailable")) { + account.removePresence(fromParts[1]); + account.deactivateGracePeriod(); + } + } + } else { + Contact contact = account.getRoster().getContact(packet.getFrom()); + if (type == null) { + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + int sizeBefore = contact.getPresences().size(); + contact.updatePresence(presence, + Presences.parseShow(packet.findChild("show"))); + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + contact.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + boolean online = sizeBefore < contact.getPresences().size(); + updateLastseen(packet, account, true); + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, online); + } else if (type.equals("unavailable")) { + if (fromParts.length != 2) { + contact.clearPresences(); + } else { + contact.removePresence(fromParts[1]); + } + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, false); + } else if (type.equals("subscribe")) { + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + mXmppConnectionService.sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + } + } + Element nick = packet.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + contact.setPresenceName(nick.getContent()); + } + } + mXmppConnectionService.updateRosterUi(); + } + + @Override + public void onPresencePacketReceived(Account account, PresencePacket packet) { + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + this.parseConferencePresence(packet, account); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + this.parseConferencePresence(packet, account); + } else { + this.parseContactPresence(packet, account); + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java new file mode 100644 index 000000000..b49cf4e61 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -0,0 +1,335 @@ +package eu.siacs.conversations.persistance; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Roster; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class DatabaseBackend extends SQLiteOpenHelper { + + private static DatabaseBackend instance = null; + + private static final String DATABASE_NAME = "history"; + private static final int DATABASE_VERSION = 8; + + private static String CREATE_CONTATCS_STATEMENT = "create table " + + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT," + + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " + + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + + Account.TABLENAME + "(" + Account.UUID + + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " + + Contact.JID + ") ON CONFLICT REPLACE);"; + + private DatabaseBackend(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("PRAGMA foreign_keys=ON;"); + db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID + + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT," + + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT," + + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS + + " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS + + " TEXT)"); + db.execSQL("create table " + Conversation.TABLENAME + " (" + + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME + + " TEXT, " + Conversation.CONTACT + " TEXT, " + + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID + + " TEXT, " + Conversation.CREATED + " NUMBER, " + + Conversation.STATUS + " NUMBER, " + Conversation.MODE + + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY(" + + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME + + "(" + Account.UUID + ") ON DELETE CASCADE);"); + db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID + + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," + + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " + + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + + Message.CONVERSATION + ") REFERENCES " + + Conversation.TABLENAME + "(" + Conversation.UUID + + ") ON DELETE CASCADE);"); + + db.execSQL(CREATE_CONTATCS_STATEMENT); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2 && newVersion >= 2) { + db.execSQL("update " + Account.TABLENAME + " set " + + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); + } + if (oldVersion < 3 && newVersion >= 3) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TYPE + " NUMBER"); + } + if (oldVersion < 5 && newVersion >= 5) { + db.execSQL("DROP TABLE " + Contact.TABLENAME); + db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL("UPDATE " + Account.TABLENAME + " SET " + + Account.ROSTERVERSION + " = NULL"); + } + if (oldVersion < 6 && newVersion >= 6) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TRUE_COUNTERPART + " TEXT"); + } + if (oldVersion < 7 && newVersion >= 7) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.REMOTE_MSG_ID + " TEXT"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.AVATAR + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + + Account.AVATAR + " TEXT"); + } + if (oldVersion < 8 && newVersion >= 8) { + db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " + + Conversation.ATTRIBUTES + " TEXT"); + } + } + + public static synchronized DatabaseBackend getInstance(Context context) { + if (instance == null) { + instance = new DatabaseBackend(context); + } + return instance; + } + + public void createConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Conversation.TABLENAME, null, conversation.getContentValues()); + } + + public void createMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Message.TABLENAME, null, message.getContentValues()); + } + + public void createAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Account.TABLENAME, null, account.getContentValues()); + } + + public void createContact(Contact contact) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } + + public int getConversationCount() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(uuid) as count from " + + Conversation.TABLENAME + " where " + Conversation.STATUS + + "=" + Conversation.STATUS_AVAILABLE, null); + cursor.moveToFirst(); + return cursor.getInt(0); + } + + public CopyOnWriteArrayList getConversations(int status) { + CopyOnWriteArrayList list = new CopyOnWriteArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { Integer.toString(status) }; + Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME + + " where " + Conversation.STATUS + " = ? order by " + + Conversation.CREATED + " desc", selectionArgs); + while (cursor.moveToNext()) { + list.add(Conversation.fromCursor(cursor)); + } + return list; + } + + public ArrayList getMessages(Conversation conversations, int limit) { + return getMessages(conversations, limit, -1); + } + + public ArrayList getMessages(Conversation conversation, int limit, + long timestamp) { + ArrayList list = new ArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + if (timestamp == -1) { + String[] selectionArgs = { conversation.getUuid() }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + + " DESC", String.valueOf(limit)); + } else { + String[] selectionArgs = { conversation.getUuid(), + Long.toString(timestamp) }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? and " + Message.TIME_SENT + " 0) { + cursor.moveToLast(); + do { + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); + } while (cursor.moveToPrevious()); + } + return list; + } + + public Conversation findConversation(Account account, String contactJid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { account.getUuid(), contactJid + "%" }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + + " like ?", selectionArgs, null, null, null); + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + return Conversation.fromCursor(cursor); + } + + public void updateConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { conversation.getUuid() }; + db.update(Conversation.TABLENAME, conversation.getContentValues(), + Conversation.UUID + "=?", args); + } + + public List getAccounts() { + List list = new ArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, + null, null); + while (cursor.moveToNext()) { + list.add(Account.fromCursor(cursor)); + } + cursor.close(); + return list; + } + + public void updateAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + + "=?", args); + } + + public void deleteAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.delete(Account.TABLENAME, Account.UUID + "=?", args); + } + + public boolean hasEnabledAccounts() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from " + + Account.TABLENAME + " where not options & (1 <<1)", null); + try { + cursor.moveToFirst(); + int count = cursor.getInt(0); + cursor.close(); + return (count > 0); + } catch (SQLiteCantOpenDatabaseException e) { + return true; // better safe than sorry + } + } + + @Override + public SQLiteDatabase getWritableDatabase() { + SQLiteDatabase db = super.getWritableDatabase(); + db.execSQL("PRAGMA foreign_keys=ON;"); + return db; + } + + public void updateMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + + "=?", args); + } + + public void readRoster(Roster roster) { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + String args[] = { roster.getAccount().getUuid() }; + cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", + args, null, null, null); + while (cursor.moveToNext()) { + roster.initContact(Contact.fromCursor(cursor)); + } + cursor.close(); + } + + public void writeRoster(Roster roster) { + Account account = roster.getAccount(); + SQLiteDatabase db = this.getWritableDatabase(); + for (Contact contact : roster.getContacts()) { + if (contact.getOption(Contact.Options.IN_ROSTER)) { + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } else { + String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; + String[] whereArgs = { account.getUuid(), contact.getJid() }; + db.delete(Contact.TABLENAME, where, whereArgs); + } + } + account.setRosterVersion(roster.getVersion()); + updateAccount(account); + } + + public void deleteMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.delete(Message.TABLENAME, Message.UUID + "=?", args); + } + + public void deleteMessagesInConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { conversation.getUuid() }; + db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); + } + + public Conversation findConversationByUuid(String conversationUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { conversationUuid }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.UUID + "=?", selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Conversation.fromCursor(cursor); + } + + public Message findMessageByUuid(String messageUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { messageUuid }; + Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Message.fromCursor(cursor); + } + + public Account findAccountByUuid(String accountUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { accountUuid }; + Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Account.fromCursor(cursor); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java new file mode 100644 index 000000000..b891e9ef5 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -0,0 +1,480 @@ +package eu.siacs.conversations.persistance; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Base64OutputStream; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class FileBackend { + + private static int IMAGE_SIZE = 1920; + + private SimpleDateFormat imageDateFormat = new SimpleDateFormat( + "yyyyMMdd_HHmmssSSS", Locale.US); + + private XmppConnectionService mXmppConnectionService; + + public FileBackend(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public DownloadableFile getFile(Message message) { + return getFile(message, true); + } + + public DownloadableFile getFile(Message message, boolean decrypted) { + StringBuilder filename = new StringBuilder(); + filename.append(getConversationsDirectory()); + filename.append(message.getUuid()); + if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { + filename.append(".webp"); + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + filename.append(".webp"); + } else { + filename.append(".webp.pgp"); + } + } + return new DownloadableFile(filename.toString()); + } + + public static String getConversationsDirectory() { + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES).getAbsolutePath() + + "/Conversations/"; + } + + public Bitmap resize(Bitmap originalBitmap, int size) { + int w = originalBitmap.getWidth(); + int h = originalBitmap.getHeight(); + if (Math.max(w, h) > size) { + int scalledW; + int scalledH; + if (w <= h) { + scalledW = (int) (w / ((double) h / size)); + scalledH = size; + } else { + scalledW = size; + scalledH = (int) (h / ((double) w / size)); + } + Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap, + scalledW, scalledH, true); + return scalledBitmap; + } else { + return originalBitmap; + } + } + + public Bitmap rotate(Bitmap bitmap, int degree) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + Matrix mtx = new Matrix(); + mtx.postRotate(degree); + return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); + } + + public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) + throws ImageCopyException { + return this.copyImageToPrivateStorage(message, image, 0); + } + + private DownloadableFile copyImageToPrivateStorage(Message message, + Uri image, int sampleSize) throws ImageCopyException { + try { + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + DownloadableFile file = getFile(message); + file.getParentFile().mkdirs(); + file.createNewFile(); + Bitmap originalBitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + int inSampleSize = (int) Math.pow(2, sampleSize); + Log.d(Config.LOGTAG, "reading bitmap with sample size " + + inSampleSize); + options.inSampleSize = inSampleSize; + originalBitmap = BitmapFactory.decodeStream(is, null, options); + is.close(); + if (originalBitmap == null) { + throw new ImageCopyException(R.string.error_not_an_image_file); + } + Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); + originalBitmap = null; + int rotation = getRotation(image); + if (rotation > 0) { + scalledBitmap = rotate(scalledBitmap, rotation); + } + OutputStream os = new FileOutputStream(file); + boolean success = scalledBitmap.compress( + Bitmap.CompressFormat.WEBP, 75, os); + if (!success) { + throw new ImageCopyException(R.string.error_compressing_image); + } + os.flush(); + os.close(); + long size = file.getSize(); + int width = scalledBitmap.getWidth(); + int height = scalledBitmap.getHeight(); + message.setBody(Long.toString(size) + ',' + width + ',' + height); + return file; + } catch (FileNotFoundException e) { + throw new ImageCopyException(R.string.error_file_not_found); + } catch (IOException e) { + throw new ImageCopyException(R.string.error_io_exception); + } catch (SecurityException e) { + throw new ImageCopyException( + R.string.error_security_exception_during_image_copy); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize <= 3) { + return copyImageToPrivateStorage(message, image, sampleSize); + } else { + throw new ImageCopyException(R.string.error_out_of_memory); + } + } + } + + private int getRotation(Uri image) { + if ("content".equals(image.getScheme())) { + try { + Cursor cursor = mXmppConnectionService + .getContentResolver() + .query(image, + new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, + null, null, null); + if (cursor.getCount() != 1) { + return -1; + } + cursor.moveToFirst(); + return cursor.getInt(0); + } catch (IllegalArgumentException e) { + return -1; + } + } else { + ExifInterface exif; + try { + exif = new ExifInterface(image.toString()); + if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("6")) { + return 90; + } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("8")) { + return 270; + } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("3")) { + return 180; + } else { + return 0; + } + } catch (IOException e) { + return -1; + } + } + } + + public Bitmap getImageFromMessage(Message message) { + return BitmapFactory.decodeFile(getFile(message).getAbsolutePath()); + } + + public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) + throws FileNotFoundException { + Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get( + message.getUuid()); + if ((thumbnail == null) && (!cacheOnly)) { + File file = getFile(message); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(file, size); + Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(), + options); + if (fullsize == null) { + throw new FileNotFoundException(); + } + thumbnail = resize(fullsize, size); + this.mXmppConnectionService.getBitmapCache().put(message.getUuid(), + thumbnail); + } + return thumbnail; + } + + public void removeFiles(Conversation conversation) { + String prefix = mXmppConnectionService.getFilesDir().getAbsolutePath(); + String path = prefix + "/" + conversation.getAccount().getJid() + "/" + + conversation.getContactJid(); + File file = new File(path); + try { + this.deleteFile(file); + } catch (IOException e) { + Log.d(Config.LOGTAG, + "error deleting file: " + file.getAbsolutePath()); + } + } + + private void deleteFile(File f) throws IOException { + if (f.isDirectory()) { + for (File c : f.listFiles()) + deleteFile(c); + } + f.delete(); + } + + public Uri getTakePhotoUri() { + StringBuilder pathBuilder = new StringBuilder(); + pathBuilder.append(Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); + pathBuilder.append('/'); + pathBuilder.append("Camera"); + pathBuilder.append('/'); + pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + + ".jpg"); + Uri uri = Uri.parse("file://" + pathBuilder.toString()); + File file = new File(uri.toString()); + file.getParentFile().mkdirs(); + return uri; + } + + public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { + try { + Avatar avatar = new Avatar(); + Bitmap bm = cropCenterSquare(image, size); + if (bm == null) { + return null; + } + ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); + Base64OutputStream mBase64OutputSttream = new Base64OutputStream( + mByteArrayOutputStream, Base64.DEFAULT); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mBase64OutputSttream, digest); + if (!bm.compress(format, 75, mDigestOutputStream)) { + return null; + } + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); + avatar.image = new String(mByteArrayOutputStream.toByteArray()); + return avatar; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (IOException e) { + return null; + } + } + + public boolean isAvatarCached(Avatar avatar) { + File file = new File(getAvatarPath(avatar.getFilename())); + return file.exists(); + } + + public boolean save(Avatar avatar) { + if (isAvatarCached(avatar)) { + return true; + } + String filename = getAvatarPath(avatar.getFilename()); + File file = new File(filename + ".tmp"); + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + FileOutputStream mFileOutputStream = new FileOutputStream(file); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mFileOutputStream, digest); + mDigestOutputStream.write(avatar.getImageAsBytes()); + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.size = file.length(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + return true; + } else { + Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); + file.delete(); + return false; + } + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + return false; + } catch (NoSuchAlgorithmException e) { + return false; + } + } + + public String getAvatarPath(String avatar) { + return mXmppConnectionService.getFilesDir().getAbsolutePath() + + "/avatars/" + avatar; + } + + public Uri getAvatarUri(String avatar) { + return Uri.parse("file:" + getAvatarPath(avatar)); + } + + public Bitmap cropCenterSquare(Uri image, int size) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, size); + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + Bitmap input = BitmapFactory.decodeStream(is, null, options); + if (input == null) { + return null; + } else { + int rotation = getRotation(image); + if (rotation > 0) { + input = rotate(input, rotation); + } + return cropCenterSquare(input, size); + } + } catch (FileNotFoundException e) { + return null; + } + } + + public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, + Math.max(newHeight, newWidth)); + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + Bitmap source = BitmapFactory.decodeStream(is, null, options); + + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + float xScale = (float) newWidth / sourceWidth; + float yScale = (float) newHeight / sourceHeight; + float scale = Math.max(xScale, yScale); + float scaledWidth = scale * sourceWidth; + float scaledHeight = scale * sourceHeight; + float left = (newWidth - scaledWidth) / 2; + float top = (newHeight - scaledHeight) / 2; + + RectF targetRect = new RectF(left, top, left + scaledWidth, top + + scaledHeight); + Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, + source.getConfig()); + Canvas canvas = new Canvas(dest); + canvas.drawBitmap(source, null, targetRect, null); + + return dest; + } catch (FileNotFoundException e) { + return null; + } + + } + + public Bitmap cropCenterSquare(Bitmap input, int size) { + int w = input.getWidth(); + int h = input.getHeight(); + + float scale = Math.max((float) size / h, (float) size / w); + + float outWidth = scale * w; + float outHeight = scale * h; + float left = (size - outWidth) / 2; + float top = (size - outHeight) / 2; + RectF target = new RectF(left, top, left + outWidth, top + outHeight); + + Bitmap output = Bitmap.createBitmap(size, size, input.getConfig()); + Canvas canvas = new Canvas(output); + canvas.drawBitmap(input, null, target, null); + return output; + } + + private int calcSampleSize(Uri image, int size) + throws FileNotFoundException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver() + .openInputStream(image), null, options); + return calcSampleSize(options, size); + } + + private int calcSampleSize(File image, int size) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(image.getAbsolutePath(), options); + return calcSampleSize(options, size); + } + + private int calcSampleSize(BitmapFactory.Options options, int size) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (height > size || width > size) { + int halfHeight = height / 2; + int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) > size + && (halfWidth / inSampleSize) > size) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + public Uri getJingleFileUri(Message message) { + File file = getFile(message); + return Uri.parse("file://" + file.getAbsolutePath()); + } + + public class ImageCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public ImageCopyException(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } + } + + public Bitmap getAvatar(String avatar, int size) { + if (avatar == null) { + return null; + } + Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); + if (bm == null) { + return null; + } + return bm; + } + + public boolean isFileAvailable(Message message) { + return getFile(message).exists(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java new file mode 100644 index 000000000..6a457b17f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.persistance; + +public interface OnPhoneContactsMerged { + public void phoneContactsMerged(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java new file mode 100644 index 000000000..676a09c97 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -0,0 +1,23 @@ +package eu.siacs.conversations.services; + +public class AbstractConnectionManager { + protected XmppConnectionService mXmppConnectionService; + + public AbstractConnectionManager(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public XmppConnectionService getXmppConnectionService() { + return this.mXmppConnectionService; + } + + public long getAutoAcceptFileSize() { + String config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size", "524288"); + try { + return Long.parseLong(config); + } catch (NumberFormatException e) { + return 524288; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java b/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java new file mode 100644 index 000000000..c0668a193 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -0,0 +1,298 @@ +package eu.siacs.conversations.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.entities.MucOptions; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.net.Uri; +import android.util.Log; + +public class AvatarService { + + private static final int FG_COLOR = 0xFFFAFAFA; + private static final int TRANSPARENT = 0x00000000; + + private static final String PREFIX_CONTACT = "contact"; + private static final String PREFIX_CONVERSATION = "conversation"; + private static final String PREFIX_ACCOUNT = "account"; + private static final String PREFIX_GENERIC = "generic"; + + private ArrayList sizes = new ArrayList(); + + protected XmppConnectionService mXmppConnectionService = null; + + public AvatarService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public Bitmap get(Contact contact, int size) { + final String KEY = key(contact, size); + Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (avatar != null) { + return avatar; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + avatar = mXmppConnectionService.getFileBackend().getAvatar( + contact.getAvatar(), size); + if (avatar == null) { + if (contact.getProfilePhoto() != null) { + avatar = mXmppConnectionService.getFileBackend() + .cropCenterSquare(Uri.parse(contact.getProfilePhoto()), + size); + if (avatar == null) { + avatar = get(contact.getDisplayName(), size); + } + } else { + avatar = get(contact.getDisplayName(), size); + } + } + this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Contact contact) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(contact, size)); + } + } + + private String key(Contact contact, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_CONTACT + "_" + contact.getAccount().getJid() + "_" + + contact.getJid() + "_" + String.valueOf(size); + } + + public Bitmap get(ListItem item, int size) { + if (item instanceof Contact) { + return get((Contact) item, size); + } else if (item instanceof Bookmark) { + Bookmark bookmark = (Bookmark) item; + if (bookmark.getConversation() != null) { + return get(bookmark.getConversation(), size); + } else { + return get(bookmark.getDisplayName(), size); + } + } else { + return get(item.getDisplayName(), size); + } + } + + public Bitmap get(Conversation conversation, int size) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + return get(conversation.getContact(), size); + } else { + return get(conversation.getMucOptions(), size); + } + } + + public void clear(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + clear(conversation.getContact()); + } else { + clear(conversation.getMucOptions()); + } + } + + public Bitmap get(MucOptions mucOptions, int size) { + final String KEY = key(mucOptions, size); + Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null) { + return bitmap; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + List users = mucOptions.getUsers(); + int count = users.size(); + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + bitmap.eraseColor(TRANSPARENT); + + if (count == 0) { + String name = mucOptions.getConversation().getName(); + String letter = name.substring(0, 1); + int color = this.getColorForName(name); + drawTile(canvas, letter, color, 0, 0, size, size); + } else if (count == 1) { + drawTile(canvas, users.get(0), 0, 0, size, size); + } else if (count == 2) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); + drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size); + } else if (count == 3) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); + drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size, + size); + } else if (count == 4) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); + drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); + drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size, + size); + } else { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); + drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); + drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, "\u2026", 0xFF202020, size / 2 + 1, size / 2 + 1, + size, size); + } + this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + return bitmap; + } + + public void clear(MucOptions options) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(options, size)); + } + } + + private String key(MucOptions options, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid() + + "_" + String.valueOf(size); + } + + public Bitmap get(Account account, int size) { + final String KEY = key(account, size); + Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY); + if (avatar != null) { + return avatar; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + avatar = mXmppConnectionService.getFileBackend().getAvatar( + account.getAvatar(), size); + if (avatar == null) { + avatar = get(account.getJid(), size); + } + mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Account account) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(account, size)); + } + } + + private String key(Account account, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_ACCOUNT + "_" + account.getUuid() + "_" + + String.valueOf(size); + } + + public Bitmap get(String name, int size) { + final String KEY = key(name, size); + Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null) { + return bitmap; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + String letter = name.substring(0, 1); + int color = this.getColorForName(name); + drawTile(canvas, letter, color, 0, 0, size, size); + mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + return bitmap; + } + + private String key(String name, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size); + } + + private void drawTile(Canvas canvas, String letter, int tileColor, + int left, int top, int right, int bottom) { + letter = letter.toUpperCase(Locale.getDefault()); + Paint tilePaint = new Paint(), textPaint = new Paint(); + tilePaint.setColor(tileColor); + textPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(FG_COLOR); + textPaint.setTypeface(Typeface.create("sans-serif-light", + Typeface.NORMAL)); + textPaint.setTextSize((float) ((right - left) * 0.8)); + Rect rect = new Rect(); + + canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); + textPaint.getTextBounds(letter, 0, 1, rect); + float width = textPaint.measureText(letter); + canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) + / 2 + rect.height() / 2, textPaint); + } + + private void drawTile(Canvas canvas, MucOptions.User user, int left, + int top, int right, int bottom) { + Contact contact = user.getContact(); + if (contact != null) { + Uri uri = null; + if (contact.getAvatar() != null) { + uri = mXmppConnectionService.getFileBackend().getAvatarUri( + contact.getAvatar()); + } else if (contact.getProfilePhoto() != null) { + uri = Uri.parse(contact.getProfilePhoto()); + } + if (uri != null) { + Bitmap bitmap = mXmppConnectionService.getFileBackend() + .cropCenter(uri, bottom - top, right - left); + if (bitmap != null) { + drawTile(canvas, bitmap, left, top, right, bottom); + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } + + private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, + int dstright, int dstbottom) { + Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom); + canvas.drawBitmap(bm, null, dst, null); + } + + private int getColorForName(String name) { + int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5, + 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722, + 0xFF795548, 0xFF607d8b }; + return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)]; + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java new file mode 100644 index 000000000..dfbe9db76 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.services; + +import eu.siacs.conversations.persistance.DatabaseBackend; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class EventReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Intent mIntentForService = new Intent(context, + XmppConnectionService.class); + if (intent.getAction() != null) { + mIntentForService.setAction(intent.getAction()); + } else { + mIntentForService.setAction("other"); + } + if (intent.getAction().equals("ui") + || DatabaseBackend.getInstance(context).hasEnabledAccounts()) { + context.startService(mIntentForService); + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java b/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java new file mode 100644 index 000000000..00765deb7 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -0,0 +1,237 @@ +package eu.siacs.conversations.services; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.PowerManager; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.text.Html; +import android.util.DisplayMetrics; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.ConversationActivity; + +public class NotificationService { + + private XmppConnectionService mXmppConnectionService; + + private LinkedHashMap> notifications = new LinkedHashMap>(); + + public int NOTIFICATION_ID = 0x2342; + private Conversation mOpenConversation; + private boolean mIsInForeground; + + public NotificationService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void push(Message message) { + PowerManager pm = (PowerManager) mXmppConnectionService + .getSystemService(Context.POWER_SERVICE); + boolean isScreenOn = pm.isScreenOn(); + + if (this.mIsInForeground && isScreenOn + && this.mOpenConversation == message.getConversation()) { + return; + } + synchronized (notifications) { + String conversationUuid = message.getConversationUuid(); + if (notifications.containsKey(conversationUuid)) { + notifications.get(conversationUuid).add(message); + } else { + ArrayList mList = new ArrayList(); + mList.add(message); + notifications.put(conversationUuid, mList); + } + Account account = message.getConversation().getAccount(); + updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) + && !account.inGracePeriod()); + } + + } + + public void clear() { + synchronized (notifications) { + notifications.clear(); + updateNotification(false); + } + } + + public void clear(Conversation conversation) { + synchronized (notifications) { + notifications.remove(conversation.getUuid()); + updateNotification(false); + } + } + + private void updateNotification(boolean notify) { + NotificationManager notificationManager = (NotificationManager) mXmppConnectionService + .getSystemService(Context.NOTIFICATION_SERVICE); + SharedPreferences preferences = mXmppConnectionService.getPreferences(); + + String ringtone = preferences.getString("notification_ringtone", null); + boolean vibrate = preferences.getBoolean("vibrate_on_notification", + true); + + if (notifications.size() == 0) { + notificationManager.cancel(NOTIFICATION_ID); + } else { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + mBuilder.setSmallIcon(R.drawable.ic_notification); + if (notifications.size() == 1) { + ArrayList messages = notifications.values().iterator() + .next(); + if (messages.size() >= 1) { + Conversation conversation = messages.get(0) + .getConversation(); + mBuilder.setLargeIcon(mXmppConnectionService + .getAvatarService().get(conversation, getPixel(64))); + mBuilder.setContentTitle(conversation.getName()); + StringBuilder text = new StringBuilder(); + for (int i = 0; i < messages.size(); ++i) { + text.append(messages.get(i).getReadableBody( + mXmppConnectionService)); + if (i != messages.size() - 1) { + text.append("\n"); + } + } + mBuilder.setStyle(new NotificationCompat.BigTextStyle() + .bigText(text.toString())); + mBuilder.setContentText(messages.get(0).getReadableBody( + mXmppConnectionService)); + if (notify) { + mBuilder.setTicker(messages.get(messages.size() - 1) + .getReadableBody(mXmppConnectionService)); + } + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } else { + notificationManager.cancel(NOTIFICATION_ID); + return; + } + } else { + NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); + style.setBigContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + StringBuilder names = new StringBuilder(); + Conversation conversation = null; + for (ArrayList messages : notifications.values()) { + if (messages.size() > 0) { + conversation = messages.get(0).getConversation(); + String name = conversation.getName(); + style.addLine(Html.fromHtml("" + + name + + " " + + messages.get(0).getReadableBody( + mXmppConnectionService))); + names.append(name); + names.append(", "); + } + } + if (names.length() >= 2) { + names.delete(names.length() - 2, names.length()); + } + mBuilder.setContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + mBuilder.setContentText(names.toString()); + mBuilder.setStyle(style); + if (conversation != null) { + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } + } + if (notify) { + if (vibrate) { + int dat = 70; + long[] pattern = { 0, 3 * dat, dat, dat }; + mBuilder.setVibrate(pattern); + } + if (ringtone != null) { + mBuilder.setSound(Uri.parse(ringtone)); + } + } + mBuilder.setDeleteIntent(createDeleteIntent()); + mBuilder.setLights(0xffffffff, 2000, 4000); + Notification notification = mBuilder.build(); + notificationManager.notify(NOTIFICATION_ID, notification); + } + } + + private PendingIntent createContentIntent(String conversationUuid) { + TaskStackBuilder stackBuilder = TaskStackBuilder + .create(mXmppConnectionService); + stackBuilder.addParentStack(ConversationActivity.class); + + Intent viewConversationIntent = new Intent(mXmppConnectionService, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversationUuid); + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + + stackBuilder.addNextIntent(viewConversationIntent); + + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); + return resultPendingIntent; + } + + private PendingIntent createDeleteIntent() { + Intent intent = new Intent(mXmppConnectionService, + XmppConnectionService.class); + intent.setAction("clear_notification"); + return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + } + + public static boolean wasHighlightedOrPrivate(Message message) { + String nick = message.getConversation().getMucOptions().getActualNick(); + Pattern highlight = generateNickHighlightPattern(nick); + if (message.getBody() == null || nick == null) { + return false; + } + Matcher m = highlight.matcher(message.getBody()); + return (m.find() || message.getType() == Message.TYPE_PRIVATE); + } + + private static Pattern generateNickHighlightPattern(String nick) { + // We expect a word boundary, i.e. space or start of string, followed by + // the + // nick (matched in case-insensitive manner), followed by optional + // punctuation (for example "bob: i disagree" or "how are you alice?"), + // followed by another word boundary. + return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + + public void setOpenConversation(Conversation conversation) { + this.mOpenConversation = conversation; + } + + public void setIsInForeground(boolean foreground) { + this.mIsInForeground = foreground; + } + + private int getPixel(int dp) { + DisplayMetrics metrics = mXmppConnectionService.getResources() + .getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java new file mode 100644 index 000000000..37e334eb6 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -0,0 +1,1927 @@ +package eu.siacs.conversations.services; + +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + +import de.duenndns.ssl.MemorizingTrustManager; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.generator.IqGenerator; +import eu.siacs.conversations.generator.MessageGenerator; +import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.parser.MessageParser; +import eu.siacs.conversations.parser.PresenceParser; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.UiCallback; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; +import eu.siacs.conversations.utils.PRNGFixes; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnBindListener; +import eu.siacs.conversations.xmpp.OnContactStatusChanged; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.OnMessageAcknowledged; +import eu.siacs.conversations.xmpp.OnStatusChanged; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.FileObserver; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.util.Log; +import android.util.LruCache; + +public class XmppConnectionService extends Service { + + public DatabaseBackend databaseBackend; + private FileBackend fileBackend = new FileBackend(this); + + public long startDate; + + private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + + private MemorizingTrustManager mMemorizingTrustManager; + + private NotificationService mNotificationService = new NotificationService( + this); + + private MessageParser mMessageParser = new MessageParser(this); + private PresenceParser mPresenceParser = new PresenceParser(this); + private IqParser mIqParser = new IqParser(this); + private MessageGenerator mMessageGenerator = new MessageGenerator(this); + private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); + + private List accounts; + private CopyOnWriteArrayList conversations = null; + private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( + this); + private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( + this); + private AvatarService mAvatarService = new AvatarService(this); + + private OnConversationUpdate mOnConversationUpdate = null; + private Integer convChangedListenerCount = 0; + private OnAccountUpdate mOnAccountUpdate = null; + private Integer accountChangedListenerCount = 0; + private OnRosterUpdate mOnRosterUpdate = null; + private Integer rosterChangedListenerCount = 0; + public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { + + @Override + public void onContactStatusChanged(Contact contact, boolean online) { + Conversation conversation = find(getConversations(), contact); + if (conversation != null) { + conversation.endOtrIfNeeded(); + if (online && (contact.getPresences().size() == 1)) { + sendUnsendMessages(conversation); + } + } + } + }; + + private SecureRandom mRandom; + + private ContentObserver contactObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + Intent intent = new Intent(getApplicationContext(), + XmppConnectionService.class); + intent.setAction(ACTION_MERGE_PHONE_CONTACTS); + startService(intent); + } + }; + + private FileObserver fileObserver = new FileObserver( + FileBackend.getConversationsDirectory()) { + + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.DELETE) { + markFileDeleted(path.split("\\.")[0]); + } + } + }; + + private final IBinder mBinder = new XmppConnectionBinder(); + private OnStatusChanged statusListener = new OnStatusChanged() { + + @Override + public void onStatusChanged(Account account) { + XmppConnection connection = account.getXmppConnection(); + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + ; + } + if (account.getStatus() == Account.STATUS_ONLINE) { + for (Conversation conversation : account.pendingConferenceLeaves) { + leaveMuc(conversation); + } + for (Conversation conversation : account.pendingConferenceJoins) { + joinMuc(conversation); + } + mJingleConnectionManager.cancelInTransmission(); + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); ++i) { + if (conversations.get(i).getAccount() == account) { + conversations.get(i).startOtrIfNeeded(); + sendUnsendMessages(conversations.get(i)); + } + } + if (connection != null && connection.getFeatures().csi()) { + if (checkListeners()) { + Log.d(Config.LOGTAG, account.getJid() + + " sending csi//inactive"); + connection.sendInactive(); + } else { + Log.d(Config.LOGTAG, account.getJid() + + " sending csi//active"); + connection.sendActive(); + } + } + syncDirtyContacts(account); + scheduleWakeupCall(Config.PING_MAX_INTERVAL, true); + } else if (account.getStatus() == Account.STATUS_OFFLINE) { + resetSendingToWaiting(account); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + int timeToReconnect = mRandom.nextInt(50) + 10; + scheduleWakeupCall(timeToReconnect, false); + } + } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) { + databaseBackend.updateAccount(account); + reconnectAccount(account, true); + } else if ((account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_NO_INTERNET)) { + if (connection != null) { + int next = connection.getTimeToNextAttempt(); + Log.d(Config.LOGTAG, account.getJid() + + ": error connecting account. try again in " + + next + "s for the " + + (connection.getAttempt() + 1) + " time"); + scheduleWakeupCall((int) (next * 1.2), false); + } + } + UIHelper.showErrorNotification(getApplicationContext(), + getAccounts()); + } + }; + + private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { + + @Override + public void onJinglePacketReceived(Account account, JinglePacket packet) { + mJingleConnectionManager.deliverPacket(account, packet); + } + }; + + private OpenPgpServiceConnection pgpServiceConnection; + private PgpEngine mPgpEngine = null; + private Intent pingIntent; + private PendingIntent pendingPingIntent = null; + private WakeLock wakeLock; + private PowerManager pm; + private OnBindListener mOnBindListener = new OnBindListener() { + + @Override + public void onBind(final Account account) { + account.getRoster().clearPresences(); + account.clearPresences(); // self presences + account.pendingConferenceJoins.clear(); + account.pendingConferenceLeaves.clear(); + fetchRosterFromServer(account); + fetchBookmarks(account); + sendPresencePacket(account, + mPresenceGenerator.sendPresence(account)); + connectMultiModeConversations(account); + updateConversationUi(); + } + }; + + private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { + + @Override + public void onMessageAcknowledged(Account account, String uuid) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + for (Message message : conversation.getMessages()) { + if ((message.getStatus() == Message.STATUS_UNSEND || message + .getStatus() == Message.STATUS_WAITING) + && message.getUuid().equals(uuid)) { + markMessage(message, Message.STATUS_SEND); + return; + } + } + } + } + } + }; + private LruCache mBitmapCache; + + public PgpEngine getPgpEngine() { + if (pgpServiceConnection.isBound()) { + if (this.mPgpEngine == null) { + this.mPgpEngine = new PgpEngine(new OpenPgpApi( + getApplicationContext(), + pgpServiceConnection.getService()), this); + } + return mPgpEngine; + } else { + return null; + } + + } + + public FileBackend getFileBackend() { + return this.fileBackend; + } + + public AvatarService getAvatarService() { + return this.mAvatarService; + } + + public Message attachImageToConversation(final Conversation conversation, + final Uri uri, final UiCallback callback) { + final Message message; + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", + Message.ENCRYPTION_DECRYPTED); + } else { + message = new Message(conversation, "", + conversation.getNextEncryption(forceEncryption())); + } + message.setPresence(conversation.getNextPresence()); + message.setType(Message.TYPE_IMAGE); + message.setStatus(Message.STATUS_OFFERED); + new Thread(new Runnable() { + + @Override + public void run() { + try { + getFileBackend().copyImageToPrivateStorage(message, uri); + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileBackend.ImageCopyException e) { + callback.error(e.getResId(), message); + } + } + }).start(); + return message; + } + + public Conversation find(Bookmark bookmark) { + return find(bookmark.getAccount(), bookmark.getJid()); + } + + public Conversation find(Account account, String jid) { + return find(getConversations(), account, jid); + } + + public class XmppConnectionBinder extends Binder { + public XmppConnectionService getService() { + return XmppConnectionService.this; + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && intent.getAction() != null) { + if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) { + mergePhoneContactsWithRoster(); + return START_STICKY; + } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) { + logoutAndSave(); + return START_NOT_STICKY; + } else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) { + mNotificationService.clear(); + } + } + this.wakeLock.acquire(); + + for (Account account : accounts) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (!hasInternetConnection()) { + account.setStatus(Account.STATUS_NO_INTERNET); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } else { + if (account.getStatus() == Account.STATUS_NO_INTERNET) { + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + if (account.getStatus() == Account.STATUS_ONLINE) { + long lastReceived = account.getXmppConnection() + .getLastPacketReceived(); + long lastSent = account.getXmppConnection() + .getLastPingSent(); + if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) { + Log.d(Config.LOGTAG, account.getJid() + + ": ping timeout"); + this.reconnectAccount(account, true); + } else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) { + account.getXmppConnection().sendPing(); + this.scheduleWakeupCall(2, false); + } + } else if (account.getStatus() == Account.STATUS_OFFLINE) { + if (account.getXmppConnection() == null) { + account.setXmppConnection(this + .createConnection(account)); + } + new Thread(account.getXmppConnection()).start(); + } else if ((account.getStatus() == Account.STATUS_CONNECTING) + && ((SystemClock.elapsedRealtime() - account + .getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) { + Log.d(Config.LOGTAG, account.getJid() + + ": time out during connect reconnecting"); + reconnectAccount(account, true); + } else { + if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { + reconnectAccount(account, true); + } + } + // in any case. reschedule wakup call + this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true); + } + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + } + } + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return START_STICKY; + } + + public boolean hasInternetConnection() { + ConnectivityManager cm = (ConnectivityManager) getApplicationContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnected(); + } + + @SuppressLint("TrulyRandom") + @Override + public void onCreate() { + ExceptionHelper.init(getApplicationContext()); + PRNGFixes.apply(); + this.mRandom = new SecureRandom(); + this.mMemorizingTrustManager = new MemorizingTrustManager( + getApplicationContext()); + + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int cacheSize = maxMemory / 8; + this.mBitmapCache = new LruCache(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + + this.databaseBackend = DatabaseBackend + .getInstance(getApplicationContext()); + this.accounts = databaseBackend.getAccounts(); + + for (Account account : this.accounts) { + this.databaseBackend.readRoster(account.getRoster()); + } + this.mergePhoneContactsWithRoster(); + this.getConversations(); + + getContentResolver().registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, true, contactObserver); + this.fileObserver.startWatching(); + this.pgpServiceConnection = new OpenPgpServiceConnection( + getApplicationContext(), "org.sufficientlysecure.keychain"); + this.pgpServiceConnection.bindToService(); + + this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "XmppConnectionService"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + this.logoutAndSave(); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + this.logoutAndSave(); + } + + private void logoutAndSave() { + for (Account account : accounts) { + databaseBackend.writeRoster(account.getRoster()); + if (account.getXmppConnection() != null) { + disconnect(account, false); + } + } + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, EventReceiver.class); + alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)); + Log.d(Config.LOGTAG, "good bye"); + stopSelf(); + } + + protected void scheduleWakeupCall(int seconds, boolean ping) { + long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000; + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + + if (ping) { + if (this.pingIntent == null) { + this.pingIntent = new Intent(context, EventReceiver.class); + this.pingIntent.setAction("ping"); + this.pingIntent.putExtra("time", timeToWake); + this.pendingPingIntent = PendingIntent.getBroadcast(context, 0, + this.pingIntent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + timeToWake, pendingPingIntent); + } else { + long scheduledTime = this.pingIntent.getLongExtra("time", 0); + if (scheduledTime < SystemClock.elapsedRealtime() + || (scheduledTime > timeToWake)) { + this.pingIntent.putExtra("time", timeToWake); + alarmManager.cancel(this.pendingPingIntent); + this.pendingPingIntent = PendingIntent.getBroadcast( + context, 0, this.pingIntent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + timeToWake, pendingPingIntent); + } + } + } else { + Intent intent = new Intent(context, EventReceiver.class); + intent.setAction("ping_check"); + PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, + intent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, + alarmIntent); + } + + } + + public XmppConnection createConnection(Account account) { + SharedPreferences sharedPref = getPreferences(); + account.setResource(sharedPref.getString("resource", "mobile") + .toLowerCase(Locale.getDefault())); + XmppConnection connection = new XmppConnection(account, this); + connection.setOnMessagePacketReceivedListener(this.mMessageParser); + connection.setOnStatusChangedListener(this.statusListener); + connection.setOnPresencePacketReceivedListener(this.mPresenceParser); + connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); + connection.setOnJinglePacketReceivedListener(this.jingleListener); + connection.setOnBindListener(this.mOnBindListener); + connection + .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + return connection; + } + + public void sendMessage(Message message) { + Account account = message.getConversation().getAccount(); + account.deactivateGracePeriod(); + Conversation conv = message.getConversation(); + MessagePacket packet = null; + boolean saveInDb = true; + boolean send = false; + if (account.getStatus() == Account.STATUS_ONLINE + && account.getXmppConnection() != null) { + if (message.getType() == Message.TYPE_IMAGE) { + if (message.getPresence() != null) { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { + conv.startOtrSession(this, message.getPresence(), + true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + mJingleConnectionManager + .createNewConnection(message); + } else if (message.getPresence() == null) { + message.setStatus(Message.STATUS_WAITING); + } + } else { + mJingleConnectionManager.createNewConnection(message); + } + } else { + message.setStatus(Message.STATUS_WAITING); + } + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { + conv.startOtrSession(this, message.getPresence(), true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + message.setPresence(conv.getOtrSession().getSessionID() + .getUserID()); + packet = mMessageGenerator.generateOtrChat(message); + send = true; + + } else if (message.getPresence() == null) { + message.setStatus(Message.STATUS_WAITING); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + message.getConversation().endOtrIfNeeded(); + failWaitingOtrMessages(message.getConversation()); + packet = mMessageGenerator.generatePgpChat(message); + send = true; + } else { + message.getConversation().endOtrIfNeeded(); + failWaitingOtrMessages(message.getConversation()); + packet = mMessageGenerator.generateChat(message); + send = true; + } + } + if (!account.getXmppConnection().getFeatures().sm() + && conv.getMode() != Conversation.MODE_MULTI) { + message.setStatus(Message.STATUS_SEND); + } + } else { + message.setStatus(Message.STATUS_WAITING); + if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + String pgpBody = message.getEncryptedBody(); + String decryptedBody = message.getBody(); + message.setBody(pgpBody); + message.setEncryption(Message.ENCRYPTION_PGP); + databaseBackend.createMessage(message); + saveInDb = false; + message.setBody(decryptedBody); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (conv.hasValidOtrSession()) { + message.setPresence(conv.getOtrSession().getSessionID() + .getUserID()); + } else if (!conv.hasValidOtrSession() + && message.getPresence() != null) { + conv.startOtrSession(this, message.getPresence(), false); + } + } + } + + } + conv.add(message); + if (saveInDb) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || saveEncryptedMessages()) { + databaseBackend.createMessage(message); + } + } + if ((send) && (packet != null)) { + sendMessagePacket(account, packet); + } + updateConversationUi(); + } + + private void sendUnsendMessages(Conversation conversation) { + for (int i = 0; i < conversation.getMessages().size(); ++i) { + int status = conversation.getMessages().get(i).getStatus(); + if (status == Message.STATUS_WAITING) { + resendMessage(conversation.getMessages().get(i)); + } + } + } + + private void resendMessage(Message message) { + Account account = message.getConversation().getAccount(); + MessagePacket packet = null; + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if (!message.getConversation().hasValidOtrSession()) { + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + message.getConversation().startOtrSession(this, + message.getPresence(), true); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.getConversation().startOtrSession(this, + presence, true); + } + } + } else { + if (message.getConversation().getOtrSession() + .getSessionStatus() == SessionStatus.ENCRYPTED) { + if (message.getType() == Message.TYPE_TEXT) { + packet = mMessageGenerator.generateOtrChat(message, + true); + } else if (message.getType() == Message.TYPE_IMAGE) { + mJingleConnectionManager.createNewConnection(message); + } + } + } + } else if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + packet = mMessageGenerator.generateChat(message, true); + } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED) + || (message.getEncryption() == Message.ENCRYPTION_PGP)) { + packet = mMessageGenerator.generatePgpChat(message, true); + } + } else if (message.getType() == Message.TYPE_IMAGE) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.setPresence(presence); + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } + } + } + if (packet != null) { + if (!account.getXmppConnection().getFeatures().sm() + && message.getConversation().getMode() != Conversation.MODE_MULTI) { + markMessage(message, Message.STATUS_SEND); + } else { + markMessage(message, Message.STATUS_UNSEND); + } + sendMessagePacket(account, packet); + } + } + + public void fetchRosterFromServer(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + if (!"".equals(account.getRosterVersion())) { + Log.d(Config.LOGTAG, account.getJid() + + ": fetching roster version " + account.getRosterVersion()); + } else { + Log.d(Config.LOGTAG, account.getJid() + ": fetching roster"); + } + iqPacket.query("jabber:iq:roster").setAttribute("ver", + account.getRosterVersion()); + account.getXmppConnection().sendIqPacket(iqPacket, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, + IqPacket packet) { + Element query = packet.findChild("query"); + if (query != null) { + account.getRoster().markAllAsNotInRoster(); + mIqParser.rosterItems(account, query); + } + } + }); + } + + public void fetchBookmarks(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + Element query = iqPacket.query("jabber:iq:private"); + query.addChild("storage", "storage:bookmarks"); + OnIqPacketReceived callback = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element query = packet.query(); + List bookmarks = new CopyOnWriteArrayList(); + Element storage = query.findChild("storage", + "storage:bookmarks"); + if (storage != null) { + for (Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + Bookmark bookmark = Bookmark.parse(item, account); + bookmarks.add(bookmark); + Conversation conversation = find(bookmark); + if (conversation != null) { + conversation.setBookmark(bookmark); + } else { + if (bookmark.autojoin()) { + conversation = findOrCreateConversation( + account, bookmark.getJid(), true); + conversation.setBookmark(bookmark); + joinMuc(conversation); + } + } + } + } + } + account.setBookmarks(bookmarks); + } + }; + sendIqPacket(account, iqPacket, callback); + + } + + public void pushBookmarks(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET); + Element query = iqPacket.query("jabber:iq:private"); + Element storage = query.addChild("storage", "storage:bookmarks"); + for (Bookmark bookmark : account.getBookmarks()) { + storage.addChild(bookmark); + } + sendIqPacket(account, iqPacket, null); + } + + private void mergePhoneContactsWithRoster() { + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new OnPhoneContactsLoadedListener() { + @Override + public void onPhoneContactsLoaded(List phoneContacts) { + for (Account account : accounts) { + account.getRoster().clearSystemAccounts(); + } + for (Bundle phoneContact : phoneContacts) { + for (Account account : accounts) { + String jid = phoneContact.getString("jid"); + Contact contact = account.getRoster() + .getContact(jid); + String systemAccount = phoneContact + .getInt("phoneid") + + "#" + + phoneContact.getString("lookup"); + contact.setSystemAccount(systemAccount); + contact.setPhotoUri(phoneContact + .getString("photouri")); + contact.setSystemName(phoneContact + .getString("displayname")); + getAvatarService().clear(contact); + } + } + } + }); + } + + public List getConversations() { + if (this.conversations == null) { + Hashtable accountLookupTable = new Hashtable(); + for (Account account : this.accounts) { + accountLookupTable.put(account.getUuid(), account); + } + this.conversations = databaseBackend + .getConversations(Conversation.STATUS_AVAILABLE); + for (Conversation conv : this.conversations) { + Account account = accountLookupTable.get(conv.getAccountUuid()); + conv.setAccount(account); + conv.setMessages(databaseBackend.getMessages(conv, 50)); + checkDeletedFiles(conv); + } + } + return this.conversations; + } + + private void checkDeletedFiles(Conversation conversation) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + } + } + } + } + + private void markFileDeleted(String uuid) { + for (Conversation conversation : getConversations()) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + updateConversationUi(); + } + return; + } + } + } + } + + public void populateWithOrderedConversations(List list) { + populateWithOrderedConversations(list, true); + } + + public void populateWithOrderedConversations(List list, + boolean includeConferences) { + list.clear(); + if (includeConferences) { + list.addAll(getConversations()); + } else { + for (Conversation conversation : getConversations()) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + list.add(conversation); + } + } + } + Collections.sort(list, new Comparator() { + @Override + public int compare(Conversation lhs, Conversation rhs) { + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); + } + + public int loadMoreMessages(Conversation conversation, long timestamp) { + List messages = databaseBackend.getMessages(conversation, 50, + timestamp); + for (Message message : messages) { + message.setConversation(conversation); + } + conversation.addAll(0, messages); + return messages.size(); + } + + public List getAccounts() { + return this.accounts; + } + + public Conversation find(List haystack, Contact contact) { + for (Conversation conversation : haystack) { + if (conversation.getContact() == contact) { + return conversation; + } + } + return null; + } + + public Conversation find(List haystack, Account account, + String jid) { + for (Conversation conversation : haystack) { + if ((account == null || conversation.getAccount().equals(account)) + && (conversation.getContactJid().split("/", 2)[0] + .equals(jid))) { + return conversation; + } + } + return null; + } + + public Conversation findOrCreateConversation(Account account, String jid, + boolean muc) { + Conversation conversation = find(account, jid); + if (conversation != null) { + return conversation; + } + conversation = databaseBackend.findConversation(account, jid); + if (conversation != null) { + conversation.setStatus(Conversation.STATUS_AVAILABLE); + conversation.setAccount(account); + if (muc) { + conversation.setMode(Conversation.MODE_MULTI); + } else { + conversation.setMode(Conversation.MODE_SINGLE); + } + conversation.setMessages(databaseBackend.getMessages(conversation, + 50)); + this.databaseBackend.updateConversation(conversation); + } else { + String conversationName; + Contact contact = account.getRoster().getContact(jid); + if (contact != null) { + conversationName = contact.getDisplayName(); + } else { + conversationName = jid.split("@")[0]; + } + if (muc) { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_MULTI); + } else { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_SINGLE); + } + this.databaseBackend.createConversation(conversation); + } + this.conversations.add(conversation); + updateConversationUi(); + return conversation; + } + + public void archiveConversation(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null && bookmark.autojoin()) { + bookmark.setAutojoin(false); + pushBookmarks(bookmark.getAccount()); + } + } + leaveMuc(conversation); + } else { + conversation.endOtrIfNeeded(); + } + this.databaseBackend.updateConversation(conversation); + this.conversations.remove(conversation); + updateConversationUi(); + } + + public void clearConversationHistory(Conversation conversation) { + this.databaseBackend.deleteMessagesInConversation(conversation); + this.fileBackend.removeFiles(conversation); + conversation.getMessages().clear(); + updateConversationUi(); + } + + public int getConversationCount() { + return this.databaseBackend.getConversationCount(); + } + + public void createAccount(Account account) { + databaseBackend.createAccount(account); + this.accounts.add(account); + this.reconnectAccount(account, false); + updateAccountUi(); + } + + public void updateAccount(Account account) { + this.statusListener.onStatusChanged(account); + databaseBackend.updateAccount(account); + reconnectAccount(account, false); + updateAccountUi(); + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); + } + + public void deleteAccount(Account account) { + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else if (conversation.getMode() == Conversation.MODE_SINGLE) { + conversation.endOtrIfNeeded(); + } + conversations.remove(conversation); + } + } + if (account.getXmppConnection() != null) { + this.disconnect(account, true); + } + databaseBackend.deleteAccount(account); + this.accounts.remove(account); + updateAccountUi(); + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); + } + + public void setOnConversationListChangedListener( + OnConversationUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, + "ignoring setOnConversationListChangedListener"); + return; + } + synchronized (this.convChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnConversationUpdate = listener; + this.mNotificationService.setIsInForeground(true); + this.convChangedListenerCount++; + } + } + + public void removeOnConversationListChangedListener() { + synchronized (this.convChangedListenerCount) { + this.convChangedListenerCount--; + if (this.convChangedListenerCount <= 0) { + this.convChangedListenerCount = 0; + this.mOnConversationUpdate = null; + this.mNotificationService.setIsInForeground(false); + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnAccountListChangedListener(OnAccountUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener"); + return; + } + synchronized (this.accountChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnAccountUpdate = listener; + this.accountChangedListenerCount++; + } + } + + public void removeOnAccountListChangedListener() { + synchronized (this.accountChangedListenerCount) { + this.accountChangedListenerCount--; + if (this.accountChangedListenerCount <= 0) { + this.mOnAccountUpdate = null; + this.accountChangedListenerCount = 0; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnRosterUpdateListener(OnRosterUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener"); + return; + } + synchronized (this.rosterChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnRosterUpdate = listener; + this.rosterChangedListenerCount++; + } + } + + public void removeOnRosterUpdateListener() { + synchronized (this.rosterChangedListenerCount) { + this.rosterChangedListenerCount--; + if (this.rosterChangedListenerCount <= 0) { + this.rosterChangedListenerCount = 0; + this.mOnRosterUpdate = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + private boolean checkListeners() { + return (this.mOnAccountUpdate == null + && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null); + } + + private void switchToForeground() { + for (Account account : getAccounts()) { + if (account.getStatus() == Account.STATUS_ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().csi()) { + connection.sendActive(); + } + } + } + Log.d(Config.LOGTAG, "app switched into foreground"); + } + + private void switchToBackground() { + for (Account account : getAccounts()) { + if (account.getStatus() == Account.STATUS_ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().csi()) { + connection.sendInactive(); + } + } + } + this.mNotificationService.setIsInForeground(false); + Log.d(Config.LOGTAG, "app switched into background"); + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) this + .getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + public void connectMultiModeConversations(Account account) { + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if ((conversation.getMode() == Conversation.MODE_MULTI) + && (conversation.getAccount() == account)) { + joinMuc(conversation); + } + } + } + + public void joinMuc(Conversation conversation) { + Account account = conversation.getAccount(); + account.pendingConferenceJoins.remove(conversation); + account.pendingConferenceLeaves.remove(conversation); + if (account.getStatus() == Account.STATUS_ONLINE) { + Log.d(Config.LOGTAG, + "joining conversation " + conversation.getContactJid()); + String nick = conversation.getMucOptions().getProposedNick(); + conversation.getMucOptions().setJoinNick(nick); + PresencePacket packet = new PresencePacket(); + String joinJid = conversation.getMucOptions().getJoinJid(); + packet.setAttribute("to", conversation.getMucOptions().getJoinJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); + if (conversation.getMucOptions().getPassword() != null) { + Element password = x.addChild("password"); + password.setContent(conversation.getMucOptions().getPassword()); + } + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + if (conversation.getMessages().size() != 0) { + final SimpleDateFormat mDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + Date date = new Date(conversation.getLatestMessage() + .getTimeSent() + 1000); + x.addChild("history").setAttribute("since", + mDateFormat.format(date)); + } + packet.addChild(x); + sendPresencePacket(account, packet); + if (!joinJid.equals(conversation.getContactJid())) { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + } + } else { + account.pendingConferenceJoins.add(conversation); + } + } + + private OnRenameListener renameListener = null; + private IqGenerator mIqGenerator = new IqGenerator(this); + + public void setOnRenameListener(OnRenameListener listener) { + this.renameListener = listener; + } + + public void providePasswordForMuc(Conversation conversation, String password) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.getMucOptions().setPassword(password); + if (conversation.getBookmark() != null) { + conversation.getBookmark().setAutojoin(true); + pushBookmarks(conversation.getAccount()); + } + databaseBackend.updateConversation(conversation); + joinMuc(conversation); + } + } + + public void renameInMuc(final Conversation conversation, final String nick) { + final MucOptions options = conversation.getMucOptions(); + options.setJoinNick(nick); + if (options.online()) { + Account account = conversation.getAccount(); + options.setOnRenameListener(new OnRenameListener() { + + @Override + public void onRename(boolean success) { + if (renameListener != null) { + renameListener.onRename(success); + } + if (success) { + conversation.setContactJid(conversation.getMucOptions() + .getJoinJid()); + databaseBackend.updateConversation(conversation); + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + pushBookmarks(bookmark.getAccount()); + } + } + } + }); + options.flagAboutToRename(); + PresencePacket packet = new PresencePacket(); + packet.setAttribute("to", options.getJoinJid()); + packet.setAttribute("from", conversation.getAccount().getFullJid()); + + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + sendPresencePacket(account, packet); + } else { + conversation.setContactJid(options.getJoinJid()); + databaseBackend.updateConversation(conversation); + if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + pushBookmarks(bookmark.getAccount()); + } + joinMuc(conversation); + } + } + } + + public void leaveMuc(Conversation conversation) { + Account account = conversation.getAccount(); + account.pendingConferenceJoins.remove(conversation); + account.pendingConferenceLeaves.remove(conversation); + if (account.getStatus() == Account.STATUS_ONLINE) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("to", conversation.getMucOptions().getJoinJid()); + packet.setAttribute("from", conversation.getAccount().getFullJid()); + packet.setAttribute("type", "unavailable"); + sendPresencePacket(conversation.getAccount(), packet); + conversation.getMucOptions().setOffline(); + conversation.deregisterWithBookmark(); + Log.d(Config.LOGTAG, conversation.getAccount().getJid() + + ": leaving muc " + conversation.getContactJid()); + } else { + account.pendingConferenceLeaves.add(conversation); + } + } + + public void disconnect(Account account, boolean force) { + if ((account.getStatus() == Account.STATUS_ONLINE) + || (account.getStatus() == Account.STATUS_DISABLED)) { + if (!force) { + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else { + if (conversation.endOtrIfNeeded()) { + Log.d(Config.LOGTAG, account.getJid() + + ": ended otr session with " + + conversation.getContactJid()); + } + } + } + } + } + account.getXmppConnection().disconnect(force); + } + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + public void updateMessage(Message message) { + databaseBackend.updateMessage(message); + updateConversationUi(); + } + + protected void syncDirtyContacts(Account account) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.getOption(Contact.Options.DIRTY_PUSH)) { + pushContactToServer(contact); + } + if (contact.getOption(Contact.Options.DIRTY_DELETE)) { + deleteContactOnServer(contact); + } + } + } + + public void createContact(Contact contact) { + SharedPreferences sharedPref = getPreferences(); + boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true); + if (autoGrant) { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + contact.setOption(Contact.Options.ASKING); + } + pushContactToServer(contact); + } + + public void onOtrSessionEstablished(Conversation conversation) { + Account account = conversation.getAccount(); + List messages = conversation.getMessages(); + Session otrSession = conversation.getOtrSession(); + Log.d(Config.LOGTAG, + account.getJid() + " otr session established with " + + conversation.getContactJid() + "/" + + otrSession.getSessionID().getUserID()); + for (int i = 0; i < messages.size(); ++i) { + Message msg = messages.get(i); + if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING) + && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { + msg.setPresence(otrSession.getSessionID().getUserID()); + if (msg.getType() == Message.TYPE_TEXT) { + MessagePacket outPacket = mMessageGenerator + .generateOtrChat(msg, true); + if (outPacket != null) { + msg.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(msg); + sendMessagePacket(account, outPacket); + } + } else if (msg.getType() == Message.TYPE_IMAGE) { + mJingleConnectionManager.createNewConnection(msg); + } + } + } + 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.getFullJid()); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setTo(otrSession.getSessionID().getAccountID() + "/" + + otrSession.getSessionID().getUserID()); + try { + packet.setBody(otrSession + .transformSending(CryptoHelper.FILETRANSFER + + CryptoHelper.bytesToHex(symmetricKey))); + sendMessagePacket(account, packet); + conversation.setSymmetricKey(symmetricKey); + return true; + } catch (OtrException e) { + return false; + } + } + return false; + } + + public void pushContactToServer(Contact contact) { + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.setOption(Contact.Options.DIRTY_PUSH); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + boolean ask = contact.getOption(Contact.Options.ASKING); + boolean sendUpdates = contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) + && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.query("jabber:iq:roster").addChild(contact.asElement()); + account.getXmppConnection().sendIqPacket(iq, null); + if (sendUpdates) { + sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } + if (ask) { + sendPresencePacket(account, + mPresenceGenerator.requestPresenceUpdatesFrom(contact)); + } + } + } + + public void publishAvatar(Account account, Uri image, + final UiCallback callback) { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = getFileBackend() + .getPepAvatar(image, size, format); + if (avatar != null) { + avatar.height = size; + avatar.width = size; + if (format.equals(Bitmap.CompressFormat.WEBP)) { + avatar.type = "image/webp"; + } else if (format.equals(Bitmap.CompressFormat.JPEG)) { + avatar.type = "image/jpeg"; + } else if (format.equals(Bitmap.CompressFormat.PNG)) { + avatar.type = "image/png"; + } + if (!getFileBackend().save(avatar)) { + callback.error(R.string.error_saving_avatar, avatar); + return; + } + IqPacket packet = this.mIqGenerator.publishAvatar(avatar); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + IqPacket packet = XmppConnectionService.this.mIqGenerator + .publishAvatarMetadata(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + callback.success(avatar); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error(R.string.error_publish_avatar_converting, null); + } + } + + public void fetchAvatar(Account account, Avatar avatar) { + fetchAvatar(account, avatar, null); + } + + public void fetchAvatar(Account account, final Avatar avatar, + final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + final String ERROR = account.getJid() + + ": fetching avatar for " + avatar.owner + " failed "; + if (result.getType() == IqPacket.TYPE_RESULT) { + avatar.image = mIqParser.avatarData(result); + if (avatar.image != null) { + if (getFileBackend().save(avatar)) { + if (account.getJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + updateConversationUi(); + updateAccountUi(); + } else { + Contact contact = account.getRoster() + .getContact(avatar.owner); + contact.setAvatar(avatar.getFilename()); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(); + } + if (callback != null) { + callback.success(avatar); + } + Log.d(Config.LOGTAG, account.getJid() + + ": succesfully fetched avatar for " + + avatar.owner); + return; + } + } else { + + Log.d(Config.LOGTAG, ERROR + "(parsing error)"); + } + } else { + Element error = result.findChild("error"); + if (error == null) { + Log.d(Config.LOGTAG, ERROR + "(server error)"); + } else { + Log.d(Config.LOGTAG, ERROR + error.toString()); + } + } + if (callback != null) { + callback.error(0, null); + } + + } + }); + } + + public void checkForAvatar(Account account, + final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = account.getJid(); + if (fileBackend.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatar(account, avatar, callback); + } + return; + } + } + } + } + callback.error(0, null); + } + }); + } + + public void deleteContactOnServer(Contact contact) { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.setOption(Contact.Options.DIRTY_DELETE); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element item = iq.query("jabber:iq:roster").addChild("item"); + item.setAttribute("jid", contact.getJid()); + item.setAttribute("subscription", "remove"); + account.getXmppConnection().sendIqPacket(iq, null); + } + } + + public void updateConversation(Conversation conversation) { + this.databaseBackend.updateConversation(conversation); + } + + public void reconnectAccount(final Account account, final boolean force) { + new Thread(new Runnable() { + + @Override + public void run() { + if (account.getXmppConnection() != null) { + disconnect(account, force); + } + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (account.getXmppConnection() == null) { + account.setXmppConnection(createConnection(account)); + } + Thread thread = new Thread(account.getXmppConnection()); + thread.start(); + scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2), + false); + } else { + account.getRoster().clearPresences(); + account.setXmppConnection(null); + } + } + }).start(); + } + + public void invite(Conversation conversation, String contact) { + MessagePacket packet = mMessageGenerator.invite(conversation, contact); + sendMessagePacket(conversation.getAccount(), packet); + } + + public void resetSendingToWaiting(Account account) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + for (Message message : conversation.getMessages()) { + if (message.getType() != Message.TYPE_IMAGE + && message.getStatus() == Message.STATUS_UNSEND) { + markMessage(message, Message.STATUS_WAITING); + } + } + } + } + } + + public boolean markMessage(Account account, String recipient, String uuid, + int status) { + if (uuid == null) { + return false; + } else { + for (Conversation conversation : getConversations()) { + if (conversation.getContactJid().equals(recipient) + && conversation.getAccount().equals(account)) { + return markMessage(conversation, uuid, status); + } + } + return false; + } + } + + public boolean markMessage(Conversation conversation, String uuid, + int status) { + if (uuid == null) { + return false; + } else { + for (Message message : conversation.getMessages()) { + if (uuid.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND && uuid + .equals(message.getRemoteMsgId()))) { + markMessage(message, status); + return true; + } + } + return false; + } + } + + public void markMessage(Message message, int status) { + if (status == Message.STATUS_SEND_FAILED + && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message + .getStatus() == Message.STATUS_SEND_DISPLAYED)) { + return; + } + message.setStatus(status); + databaseBackend.updateMessage(message); + updateConversationUi(); + } + + public SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + } + + public boolean forceEncryption() { + return getPreferences().getBoolean("force_encryption", false); + } + + public boolean confirmMessages() { + return getPreferences().getBoolean("confirm_messages", true); + } + + public boolean saveEncryptedMessages() { + return !getPreferences().getBoolean("dont_save_encrypted", false); + } + + public boolean indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + public void updateConversationUi() { + if (mOnConversationUpdate != null) { + mOnConversationUpdate.onConversationUpdate(); + } + } + + public void updateAccountUi() { + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + } + + public void updateRosterUi() { + if (mOnRosterUpdate != null) { + mOnRosterUpdate.onRosterUpdate(); + } + } + + public Account findAccountByJid(String accountJid) { + for (Account account : this.accounts) { + if (account.getJid().equals(accountJid)) { + return account; + } + } + return null; + } + + public Conversation findConversationByUuid(String uuid) { + for (Conversation conversation : getConversations()) { + if (conversation.getUuid().equals(uuid)) { + return conversation; + } + } + return null; + } + + public void markRead(Conversation conversation, boolean calledByUi) { + mNotificationService.clear(conversation); + String id = conversation.getLatestMarkableMessageId(); + conversation.markRead(); + if (confirmMessages() && id != null && calledByUi) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid() + + ": sending read marker for " + conversation.getName()); + Account account = conversation.getAccount(); + String to = conversation.getContactJid(); + this.sendMessagePacket(conversation.getAccount(), + mMessageGenerator.confirm(account, to, id)); + } + if (!calledByUi) { + updateConversationUi(); + } + } + + public void failWaitingOtrMessages(Conversation conversation) { + for (Message message : conversation.getMessages()) { + if (message.getEncryption() == Message.ENCRYPTION_OTR + && message.getStatus() == Message.STATUS_WAITING) { + markMessage(message, Message.STATUS_SEND_FAILED); + } + } + } + + public SecureRandom getRNG() { + return this.mRandom; + } + + public MemorizingTrustManager getMemorizingTrustManager() { + return this.mMemorizingTrustManager; + } + + public PowerManager getPowerManager() { + return this.pm; + } + + public LruCache getBitmapCache() { + return this.mBitmapCache; + } + + public void replyWithNotAcceptable(Account account, MessagePacket packet) { + if (account.getStatus() == Account.STATUS_ONLINE) { + MessagePacket error = this.mMessageGenerator + .generateNotAcceptable(packet); + sendMessagePacket(account, error); + } + } + + public void syncRosterToDisk(final Account account) { + new Thread(new Runnable() { + + @Override + public void run() { + databaseBackend.writeRoster(account.getRoster()); + } + }).start(); + + } + + public List getKnownHosts() { + List hosts = new ArrayList(); + for (Account account : getAccounts()) { + if (!hosts.contains(account.getServer())) { + hosts.add(account.getServer()); + } + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster()) { + String server = contact.getServer(); + if (server != null && !hosts.contains(server)) { + hosts.add(server); + } + } + } + } + return hosts; + } + + public List getKnownConferenceHosts() { + ArrayList mucServers = new ArrayList(); + for (Account account : accounts) { + if (account.getXmppConnection() != null) { + String server = account.getXmppConnection().getMucServer(); + if (server != null && !mucServers.contains(server)) { + mucServers.add(server); + } + } + } + return mucServers; + } + + public void sendMessagePacket(Account account, MessagePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendMessagePacket(packet); + } + } + + public void sendPresencePacket(Account account, PresencePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendPresencePacket(packet); + } + } + + public void sendIqPacket(Account account, IqPacket packet, + OnIqPacketReceived callback) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendIqPacket(packet, callback); + } + } + + public MessageGenerator getMessageGenerator() { + return this.mMessageGenerator; + } + + public PresenceGenerator getPresenceGenerator() { + return this.mPresenceGenerator; + } + + public IqGenerator getIqGenerator() { + return this.mIqGenerator; + } + + public JingleConnectionManager getJingleConnectionManager() { + return this.mJingleConnectionManager; + } + + public interface OnConversationUpdate { + public void onConversationUpdate(); + } + + public interface OnAccountUpdate { + public void onAccountUpdate(); + } + + public interface OnRosterUpdate { + public void onRosterUpdate(); + } + + public List findContacts(String jid) { + ArrayList contacts = new ArrayList(); + for (Account account : getAccounts()) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + Contact contact = account.getRoster().getContactFromRoster(jid); + if (contact != null) { + contacts.add(contact); + } + } + } + return contacts; + } + + public NotificationService getNotificationService() { + return this.mNotificationService; + } + + public HttpConnectionManager getHttpConnectionManager() { + return this.mHttpConnectionManager; + } + + private class DeletedDownloadable implements Downloadable { + + @Override + public boolean start() { + return false; + } + + @Override + public int getStatus() { + return Downloadable.STATUS_DELETED; + } + + @Override + public long getFileSize() { + return 0; + } + + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java new file mode 100644 index 000000000..62a2cbe15 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -0,0 +1,145 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Collections; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.adapter.ListItemAdapter; + +public class ChooseContactActivity extends XmppActivity { + + private ListView mListView; + private ArrayList contacts = new ArrayList(); + private ArrayAdapter mContactsAdapter; + + private EditText mSearchEditText; + + private TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable editable) { + filterContacts(editable.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + }; + + private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filterContacts(null); + return true; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_choose_contact); + mListView = (ListView) findViewById(R.id.choose_contact_list); + mListView.setFastScrollEnabled(true); + mContactsAdapter = new ListItemAdapter(this, contacts); + mListView.setAdapter(mContactsAdapter); + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + Intent request = getIntent(); + Intent data = new Intent(); + ListItem mListItem = contacts.get(position); + data.putExtra("contact", mListItem.getJid()); + String account = request.getStringExtra("account"); + if (account == null && mListItem instanceof Contact) { + account = ((Contact) mListItem).getAccount().getJid(); + } + data.putExtra("account", account); + data.putExtra("conversation", + request.getStringExtra("conversation")); + setResult(RESULT_OK, data); + finish(); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.choose_contact, menu); + MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search); + View mSearchView = menuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + menuSearchView.setOnActionExpandListener(mOnActionExpandListener); + return true; + } + + @Override + void onBackendConnected() { + filterContacts(null); + } + + protected void filterContacts(String needle) { + this.contacts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && contact.match(needle)) { + this.contacts.add(contact); + } + } + } + } + Collections.sort(this.contacts); + mContactsAdapter.notifyDataSetChanged(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java new file mode 100644 index 000000000..6b4642cbe --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -0,0 +1,280 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.MucOptions.User; +import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import android.app.PendingIntent; +import android.content.Context; +import android.content.IntentSender.SendIntentException; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class ConferenceDetailsActivity extends XmppActivity { + public static final String ACTION_VIEW_MUC = "view_muc"; + private Conversation conversation; + private TextView mYourNick; + private ImageView mYourPhoto; + private ImageButton mEditNickButton; + private TextView mRoleAffiliaton; + private TextView mFullJid; + private TextView mAccountJid; + private LinearLayout membersView; + private LinearLayout mMoreDetails; + private Button mInviteButton; + private String uuid = null; + + private OnClickListener inviteListener = new OnClickListener() { + + @Override + public void onClick(View v) { + inviteToConversation(conversation); + } + }; + + private List users = new ArrayList(); + private OnConversationUpdate onConvChanged = new OnConversationUpdate() { + + @Override + public void onConversationUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_muc_details); + mYourNick = (TextView) findViewById(R.id.muc_your_nick); + mYourPhoto = (ImageView) findViewById(R.id.your_photo); + mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button); + mFullJid = (TextView) findViewById(R.id.muc_jabberid); + membersView = (LinearLayout) findViewById(R.id.muc_members); + mAccountJid = (TextView) findViewById(R.id.details_account); + mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details); + mMoreDetails.setVisibility(View.GONE); + mInviteButton = (Button) findViewById(R.id.invite); + mInviteButton.setOnClickListener(inviteListener); + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + mEditNickButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + quickEdit(conversation.getMucOptions().getActualNick(), + new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + xmppConnectionService.renameInMuc(conversation, + value); + } + }); + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.action_edit_subject: + if (conversation != null) { + quickEdit(conversation.getName(), new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + MessagePacket packet = xmppConnectionService + .getMessageGenerator().conferenceSubject( + conversation, value); + xmppConnectionService.sendMessagePacket( + conversation.getAccount(), packet); + } + }); + } + break; + } + return super.onOptionsItemSelected(menuItem); + } + + public String getReadableRole(int role) { + switch (role) { + case User.ROLE_MODERATOR: + return getString(R.string.moderator); + case User.ROLE_PARTICIPANT: + return getString(R.string.participant); + case User.ROLE_VISITOR: + return getString(R.string.visitor); + default: + return ""; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.muc_details, menu); + return true; + } + + @Override + void onBackendConnected() { + registerListener(); + if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { + this.uuid = getIntent().getExtras().getString("uuid"); + } + if (uuid != null) { + this.conversation = xmppConnectionService + .findConversationByUuid(uuid); + if (this.conversation != null) { + populateView(); + } + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnConversationListChangedListener(); + } + super.onStop(); + } + + protected void registerListener() { + xmppConnectionService + .setOnConversationListChangedListener(this.onConvChanged); + xmppConnectionService.setOnRenameListener(new OnRenameListener() { + + @Override + public void onRename(final boolean success) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + if (success) { + Toast.makeText( + ConferenceDetailsActivity.this, + getString(R.string.your_nick_has_been_changed), + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(ConferenceDetailsActivity.this, + getString(R.string.nick_in_use), + Toast.LENGTH_SHORT).show(); + } + } + }); + } + }); + } + + private void populateView() { + mAccountJid.setText(getString(R.string.using_account, conversation + .getAccount().getJid())); + mYourPhoto.setImageBitmap(avatarService().get( + conversation.getAccount(), getPixel(48))); + setTitle(conversation.getName()); + mFullJid.setText(conversation.getContactJid().split("/", 2)[0]); + mYourNick.setText(conversation.getMucOptions().getActualNick()); + mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); + if (conversation.getMucOptions().online()) { + mMoreDetails.setVisibility(View.VISIBLE); + User self = conversation.getMucOptions().getSelf(); + switch (self.getAffiliation()) { + case User.AFFILIATION_ADMIN: + mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " (" + + getString(R.string.admin) + ")"); + break; + case User.AFFILIATION_OWNER: + mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " (" + + getString(R.string.owner) + ")"); + break; + default: + mRoleAffiliaton.setText(getReadableRole(self.getRole())); + break; + } + } + this.users.clear(); + this.users.addAll(conversation.getMucOptions().getUsers()); + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + membersView.removeAllViews(); + for (final User user : conversation.getMucOptions().getUsers()) { + View view = (View) inflater.inflate(R.layout.contact, membersView, + false); + TextView name = (TextView) view + .findViewById(R.id.contact_display_name); + TextView key = (TextView) view.findViewById(R.id.key); + TextView role = (TextView) view.findViewById(R.id.contact_jid); + if (user.getPgpKeyId() != 0) { + key.setVisibility(View.VISIBLE); + key.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + viewPgpKey(user); + } + }); + key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); + } + Bitmap bm; + Contact contact = user.getContact(); + if (contact != null) { + bm = avatarService().get(contact, getPixel(48)); + name.setText(contact.getDisplayName()); + role.setText(user.getName() + " \u2022 " + + getReadableRole(user.getRole())); + } else { + bm = avatarService().get(user.getName(), getPixel(48)); + name.setText(user.getName()); + role.setText(getReadableRole(user.getRole())); + } + ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); + iv.setImageBitmap(bm); + membersView.addView(view); + } + } + + private void viewPgpKey(User user) { + PgpEngine pgp = xmppConnectionService.getPgpEngine(); + if (pgp != null) { + PendingIntent intent = pgp.getIntentForKey( + conversation.getAccount(), user.getPgpKeyId()); + if (intent != null) { + try { + startIntentSenderForResult(intent.getIntentSender(), 0, + null, 0, 0, 0); + } catch (SendIntentException e) { + + } + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java new file mode 100644 index 000000000..ae26466e3 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -0,0 +1,436 @@ +package eu.siacs.conversations.ui; + +import java.util.Iterator; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Intents; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.CheckBox; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.CompoundButton; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.utils.UIHelper; + +public class ContactDetailsActivity extends XmppActivity { + public static final String ACTION_VIEW_CONTACT = "view_contact"; + + private Contact contact; + + private String accountJid; + private String contactJid; + + private TextView contactJidTv; + private TextView accountJidTv; + private TextView status; + private TextView lastseen; + private CheckBox send; + private CheckBox receive; + private QuickContactBadge badge; + + private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ContactDetailsActivity.this.xmppConnectionService + .deleteContactOnServer(contact); + ContactDetailsActivity.this.finish(); + } + }; + + private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid()); + intent.putExtra(Intents.Insert.IM_PROTOCOL, + CommonDataKinds.Im.PROTOCOL_JABBER); + intent.putExtra("finishActivityOnSaveCompleted", true); + ContactDetailsActivity.this.startActivityForResult(intent, 0); + } + }; + private OnClickListener onBadgeClick = new OnClickListener() { + + @Override + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder( + ContactDetailsActivity.this); + builder.setTitle(getString(R.string.action_add_phone_book)); + builder.setMessage(getString(R.string.add_phone_book_text, + contact.getJid())); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add), addToPhonebook); + builder.create().show(); + } + }; + + private LinearLayout keys; + + private OnRosterUpdate rosterUpdate = new OnRosterUpdate() { + + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + if (contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + xmppConnectionService.sendPresencePacket(contact + .getAccount(), + xmppConnectionService.getPresenceGenerator() + .sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + } + } else { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .stopPresenceUpdatesTo(contact)); + } + } + }; + + private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } else { + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .stopPresenceUpdatesFrom(contact)); + } + } + }; + + private OnAccountUpdate accountUpdate = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { + this.accountJid = getIntent().getExtras().getString("account"); + this.contactJid = getIntent().getExtras().getString("contact"); + } + setContentView(R.layout.activity_contact_details); + + contactJidTv = (TextView) findViewById(R.id.details_contactjid); + accountJidTv = (TextView) findViewById(R.id.details_account); + status = (TextView) findViewById(R.id.details_contactstatus); + lastseen = (TextView) findViewById(R.id.details_lastseen); + send = (CheckBox) findViewById(R.id.details_send_presence); + receive = (CheckBox) findViewById(R.id.details_receive_presence); + badge = (QuickContactBadge) findViewById(R.id.details_contact_badge); + keys = (LinearLayout) findViewById(R.id.details_contact_keys); + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(getString(R.string.cancel), null); + switch (menuItem.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.action_delete_contact: + builder.setTitle(getString(R.string.action_delete_contact)) + .setMessage( + getString(R.string.remove_contact_text, + contact.getJid())) + .setPositiveButton(getString(R.string.delete), + removeFromRoster).create().show(); + break; + case R.id.action_edit_contact: + if (contact.getSystemAccount() == null) { + quickEdit(contact.getDisplayName(), new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + contact.setServerName(value); + ContactDetailsActivity.this.xmppConnectionService + .pushContactToServer(contact); + populateView(); + } + }); + } else { + Intent intent = new Intent(Intent.ACTION_EDIT); + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + Uri uri = Contacts.getLookupUri(id, systemAccount[1]); + intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE); + intent.putExtra("finishActivityOnSaveCompleted", true); + startActivity(intent); + } + break; + } + return super.onOptionsItemSelected(menuItem); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.contact_details, menu); + return true; + } + + private void populateView() { + send.setOnCheckedChangeListener(null); + receive.setOnCheckedChangeListener(null); + setTitle(contact.getDisplayName()); + if (contact.getOption(Contact.Options.FROM)) { + send.setText(R.string.send_presence_updates); + send.setChecked(true); + } else if (contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + send.setChecked(false); + send.setText(R.string.send_presence_updates); + } else { + send.setText(R.string.preemptively_grant); + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + send.setChecked(true); + } else { + send.setChecked(false); + } + } + if (contact.getOption(Contact.Options.TO)) { + receive.setText(R.string.receive_presence_updates); + receive.setChecked(true); + } else { + receive.setText(R.string.ask_for_presence_updates); + if (contact.getOption(Contact.Options.ASKING)) { + receive.setChecked(true); + } else { + receive.setChecked(false); + } + } + if (contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + receive.setEnabled(true); + send.setEnabled(true); + } else { + receive.setEnabled(false); + send.setEnabled(false); + } + + send.setOnCheckedChangeListener(this.mOnSendCheckedChange); + receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange); + + lastseen.setText(UIHelper.lastseen(getApplicationContext(), + contact.lastseen.time)); + + switch (contact.getMostAvailableStatus()) { + case Presences.CHAT: + status.setText(R.string.contact_status_free_to_chat); + status.setTextColor(mColorGreen); + break; + case Presences.ONLINE: + status.setText(R.string.contact_status_online); + status.setTextColor(mColorGreen); + break; + case Presences.AWAY: + status.setText(R.string.contact_status_away); + status.setTextColor(mColorOrange); + break; + case Presences.XA: + status.setText(R.string.contact_status_extended_away); + status.setTextColor(mColorOrange); + break; + case Presences.DND: + status.setText(R.string.contact_status_do_not_disturb); + status.setTextColor(mColorRed); + break; + case Presences.OFFLINE: + status.setText(R.string.contact_status_offline); + status.setTextColor(mSecondaryTextColor); + break; + default: + status.setText(R.string.contact_status_offline); + status.setTextColor(mSecondaryTextColor); + break; + } + if (contact.getPresences().size() > 1) { + contactJidTv.setText(contact.getJid() + " (" + + contact.getPresences().size() + ")"); + } else { + contactJidTv.setText(contact.getJid()); + } + accountJidTv.setText(getString(R.string.using_account, contact + .getAccount().getJid())); + prepareContactBadge(badge, contact); + if (contact.getSystemAccount() == null) { + badge.setOnClickListener(onBadgeClick); + } + + keys.removeAllViews(); + boolean hasKeys = false; + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + for (Iterator iterator = contact.getOtrFingerprints() + .iterator(); iterator.hasNext();) { + hasKeys = true; + final String otrFingerprint = iterator.next(); + View view = (View) inflater.inflate(R.layout.contact_key, keys, + false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + ImageButton remove = (ImageButton) view + .findViewById(R.id.button_remove); + remove.setVisibility(View.VISIBLE); + keyType.setText("OTR Fingerprint"); + key.setText(otrFingerprint); + keys.addView(view); + remove.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + confirmToDeleteFingerprint(otrFingerprint); + } + }); + } + if (contact.getPgpKeyId() != 0) { + hasKeys = true; + View view = (View) inflater.inflate(R.layout.contact_key, keys, + false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + keyType.setText("PGP Key ID"); + key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId())); + view.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService + .getPgpEngine(); + if (pgp != null) { + PendingIntent intent = pgp.getIntentForKey(contact); + if (intent != null) { + try { + startIntentSenderForResult( + intent.getIntentSender(), 0, null, 0, + 0, 0); + } catch (SendIntentException e) { + + } + } + } + } + }); + keys.addView(view); + } + if (hasKeys) { + keys.setVisibility(View.VISIBLE); + } else { + keys.setVisibility(View.GONE); + } + } + + private void prepareContactBadge(QuickContactBadge badge, Contact contact) { + if (contact.getSystemAccount() != null) { + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); + } + badge.setImageBitmap(avatarService().get(contact, getPixel(72))); + } + + protected void confirmToDeleteFingerprint(final String fingerprint) { + 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, + new android.content.DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (contact.deleteOtrFingerprint(fingerprint)) { + populateView(); + xmppConnectionService.syncRosterToDisk(contact + .getAccount()); + } + } + + }); + builder.create().show(); + } + + @Override + public void onBackendConnected() { + xmppConnectionService.setOnRosterUpdateListener(this.rosterUpdate); + xmppConnectionService + .setOnAccountListChangedListener(this.accountUpdate); + if ((accountJid != null) && (contactJid != null)) { + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + return; + } + this.contact = account.getRoster().getContact(contactJid); + populateView(); + } + } + + @Override + protected void onStop() { + super.onStop(); + xmppConnectionService.removeOnRosterUpdateListener(); + xmppConnectionService.removeOnAccountListChangedListener(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java new file mode 100644 index 000000000..91e1c81f9 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -0,0 +1,947 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import eu.siacs.conversations.utils.ExceptionHelper; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.FragmentTransaction; +import android.app.PendingIntent; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.IntentSender.SendIntentException; +import android.content.Intent; +import android.support.v4.widget.SlidingPaneLayout; +import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.Toast; + +public class ConversationActivity extends XmppActivity implements + OnAccountUpdate, OnConversationUpdate, OnRosterUpdate { + + public static final String VIEW_CONVERSATION = "viewConversation"; + public static final String CONVERSATION = "conversationUuid"; + public static final String TEXT = "text"; + public static final String PRESENCE = "eu.siacs.conversations.presence"; + + public static final int REQUEST_SEND_MESSAGE = 0x0201; + public static final int REQUEST_DECRYPT_PGP = 0x0202; + private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203; + private static final int REQUEST_IMAGE_CAPTURE = 0x0204; + private static final int REQUEST_RECORD_AUDIO = 0x0205; + private static final int REQUEST_SEND_PGP_IMAGE = 0x0206; + public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; + + private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; + private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303; + private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; + private static final String STATE_PANEL_OPEN = "state_panel_open"; + + private String mOpenConverstaion = null; + private boolean mPanelOpen = true; + + private View mContentView; + + private List conversationList = new ArrayList(); + private Conversation selectedConversation = null; + private ListView listView; + + private boolean paneShouldBeOpen = true; + private ArrayAdapter listAdapter; + + private Toast prepareImageToast; + + private Uri pendingImageUri = null; + + public List getConversationList() { + return this.conversationList; + } + + public Conversation getSelectedConversation() { + return this.selectedConversation; + } + + public void setSelectedConversation(Conversation conversation) { + this.selectedConversation = conversation; + } + + public ListView getConversationListView() { + return this.listView; + } + + public boolean shouldPaneBeOpen() { + return paneShouldBeOpen; + } + + public void showConversationsOverview() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.openPane(); + } + } + + public void hideConversationsOverview() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.closePane(); + } + } + + public boolean isConversationsOverviewHideable() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + return mSlidingPaneLayout.isSlideable(); + } else { + return false; + } + } + + public boolean isConversationsOverviewVisable() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + return mSlidingPaneLayout.isOpen(); + } else { + return true; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mOpenConverstaion = savedInstanceState.getString( + STATE_OPEN_CONVERSATION, null); + mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true); + } + + setContentView(R.layout.fragment_conversations_overview); + + listView = (ListView) findViewById(R.id.list); + + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + + this.listAdapter = new ConversationAdapter(this, conversationList); + listView.setAdapter(this.listAdapter); + + listView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View clickedView, + int position, long arg3) { + paneShouldBeOpen = false; + if (getSelectedConversation() != conversationList.get(position)) { + setSelectedConversation(conversationList.get(position)); + swapConversationFragment(); + } else { + hideConversationsOverview(); + } + } + }); + mContentView = findViewById(R.id.content_view_spl); + if (mContentView == null) { + mContentView = findViewById(R.id.content_view_ll); + } + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.setParallaxDistance(150); + mSlidingPaneLayout + .setShadowResource(R.drawable.es_slidingpane_shadow); + mSlidingPaneLayout.setSliderFadeColor(0); + mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() { + + @Override + public void onPanelOpened(View arg0) { + paneShouldBeOpen = true; + ActionBar ab = getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(false); + ab.setHomeButtonEnabled(false); + ab.setTitle(R.string.app_name); + } + invalidateOptionsMenu(); + hideKeyboard(); + if (xmppConnectionServiceBound) { + xmppConnectionService.getNotificationService() + .setOpenConversation(null); + } + } + + @Override + public void onPanelClosed(View arg0) { + paneShouldBeOpen = false; + if ((conversationList.size() > 0) + && (getSelectedConversation() != null)) { + openConversation(getSelectedConversation()); + if (!getSelectedConversation().isRead()) { + xmppConnectionService.markRead( + getSelectedConversation(), true); + listView.invalidateViews(); + } + } + } + + @Override + public void onPanelSlide(View arg0, float arg1) { + // TODO Auto-generated method stub + + } + }); + } + } + + public void openConversation(Conversation conversation) { + ActionBar ab = getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + ab.setHomeButtonEnabled(true); + if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE + || ConversationActivity.this + .useSubjectToIdentifyConference()) { + ab.setTitle(getSelectedConversation().getName()); + } else { + ab.setTitle(getSelectedConversation().getContactJid() + .split("/")[0]); + } + } + invalidateOptionsMenu(); + if (xmppConnectionServiceBound) { + xmppConnectionService.getNotificationService().setOpenConversation( + conversation); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.conversations, menu); + MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security); + MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive); + MenuItem menuMucDetails = (MenuItem) menu + .findItem(R.id.action_muc_details); + MenuItem menuContactDetails = (MenuItem) menu + .findItem(R.id.action_contact_details); + MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file); + MenuItem menuClearHistory = (MenuItem) menu + .findItem(R.id.action_clear_history); + MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add); + MenuItem menuInviteContact = (MenuItem) menu + .findItem(R.id.action_invite); + MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute); + + if (isConversationsOverviewVisable() + && isConversationsOverviewHideable()) { + menuArchive.setVisible(false); + menuMucDetails.setVisible(false); + menuContactDetails.setVisible(false); + menuSecure.setVisible(false); + menuInviteContact.setVisible(false); + menuAttach.setVisible(false); + menuClearHistory.setVisible(false); + menuMute.setVisible(false); + } else { + menuAdd.setVisible(!isConversationsOverviewHideable()); + if (this.getSelectedConversation() != null) { + if (this.getSelectedConversation().getLatestMessage() + .getEncryption() != Message.ENCRYPTION_NONE) { + menuSecure.setIcon(R.drawable.ic_action_secure); + } + if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { + menuContactDetails.setVisible(false); + menuAttach.setVisible(false); + } else { + menuMucDetails.setVisible(false); + menuInviteContact.setVisible(false); + } + } + } + return true; + } + + private void selectPresenceToAttachFile(final int attachmentChoice) { + selectPresence(getSelectedConversation(), new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) { + pendingImageUri = xmppConnectionService.getFileBackend() + .getTakePhotoUri(); + Intent takePictureIntent = new Intent( + MediaStore.ACTION_IMAGE_CAPTURE); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, + pendingImageUri); + if (takePictureIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(takePictureIntent, + REQUEST_IMAGE_CAPTURE); + } + } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG); + } else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { + Intent intent = new Intent( + MediaStore.Audio.Media.RECORD_SOUND_ACTION); + startActivityForResult(intent, REQUEST_RECORD_AUDIO); + } + } + }); + } + + private void attachFile(final int attachmentChoice) { + final Conversation conversation = getSelectedConversation(); + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + if (hasPgp()) { + if (conversation.getContact().getPgpKeyId() != 0) { + xmppConnectionService.getPgpEngine().hasKey( + conversation.getContact(), + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + ConversationActivity.this.runIntent(pi, + attachmentChoice); + } + + @Override + public void success(Contact contact) { + selectPresenceToAttachFile(attachmentChoice); + } + + @Override + public void error(int error, Contact contact) { + displayErrorDialog(error); + } + }); + } else { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + fragment.showNoPGPKeyDialog(false, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + xmppConnectionService.databaseBackend + .updateConversation(conversation); + selectPresenceToAttachFile(attachmentChoice); + } + }); + } + } + } else { + showInstallPgpDialog(); + } + } else if (getSelectedConversation().getNextEncryption( + forceEncryption()) == Message.ENCRYPTION_NONE) { + selectPresenceToAttachFile(attachmentChoice); + } else { + selectPresenceToAttachFile(attachmentChoice); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + showConversationsOverview(); + return true; + } else if (item.getItemId() == R.id.action_add) { + startActivity(new Intent(this, StartConversationActivity.class)); + return true; + } else if (getSelectedConversation() != null) { + switch (item.getItemId()) { + case R.id.action_attach_file: + attachFileDialog(); + break; + case R.id.action_archive: + this.endConversation(getSelectedConversation()); + break; + case R.id.action_contact_details: + Contact contact = this.getSelectedConversation().getContact(); + if (contact.showInRoster()) { + switchToContactDetails(contact); + } else { + showAddToRosterDialog(getSelectedConversation()); + } + break; + case R.id.action_muc_details: + Intent intent = new Intent(this, + ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", getSelectedConversation().getUuid()); + startActivity(intent); + break; + case R.id.action_invite: + inviteToConversation(getSelectedConversation()); + break; + case R.id.action_security: + selectEncryptionDialog(getSelectedConversation()); + break; + case R.id.action_clear_history: + clearHistoryDialog(getSelectedConversation()); + break; + case R.id.action_mute: + muteConversationDialog(getSelectedConversation()); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } else { + return super.onOptionsItemSelected(item); + } + } + + public void endConversation(Conversation conversation) { + conversation.setStatus(Conversation.STATUS_ARCHIVED); + paneShouldBeOpen = true; + showConversationsOverview(); + xmppConnectionService.archiveConversation(conversation); + if (conversationList.size() > 0) { + setSelectedConversation(conversationList.get(0)); + } else { + setSelectedConversation(null); + } + } + + @SuppressLint("InflateParams") + protected void clearHistoryDialog(final Conversation conversation) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.clear_conversation_history)); + View dialogView = getLayoutInflater().inflate( + R.layout.dialog_clear_history, null); + final CheckBox endConversationCheckBox = (CheckBox) dialogView + .findViewById(R.id.end_conversation_checkbox); + builder.setView(dialogView); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.delete_messages), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ConversationActivity.this.xmppConnectionService + .clearConversationHistory(conversation); + if (endConversationCheckBox.isChecked()) { + endConversation(conversation); + } + } + }); + builder.create().show(); + } + + protected void attachFileDialog() { + View menuAttachFile = findViewById(R.id.action_attach_file); + if (menuAttachFile == null) { + return; + } + PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile); + attachFilePopup.inflate(R.menu.attachment_choices); + attachFilePopup + .setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.attach_choose_picture: + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + break; + case R.id.attach_take_picture: + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case R.id.attach_record_voice: + attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + break; + } + return false; + } + }); + attachFilePopup.show(); + } + + protected void selectEncryptionDialog(final Conversation conversation) { + View menuItemView = findViewById(R.id.action_security); + if (menuItemView == null) { + return; + } + PopupMenu popup = new PopupMenu(this, menuItemView); + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.encryption_choice_none: + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + item.setChecked(true); + break; + case R.id.encryption_choice_otr: + conversation.setNextEncryption(Message.ENCRYPTION_OTR); + item.setChecked(true); + break; + case R.id.encryption_choice_pgp: + if (hasPgp()) { + if (conversation.getAccount().getKeys() + .has("pgp_signature")) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + item.setChecked(true); + } else { + announcePgp(conversation.getAccount(), + conversation); + } + } else { + showInstallPgpDialog(); + } + break; + default: + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + break; + } + xmppConnectionService.databaseBackend + .updateConversation(conversation); + fragment.updateChatMsgHint(); + return true; + } + }); + popup.inflate(R.menu.encryption_choices); + MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr); + MenuItem none = popup.getMenu().findItem( + R.id.encryption_choice_none); + if (conversation.getMode() == Conversation.MODE_MULTI) { + otr.setEnabled(false); + } else { + if (forceEncryption()) { + none.setVisible(false); + } + } + switch (conversation.getNextEncryption(forceEncryption())) { + case Message.ENCRYPTION_NONE: + none.setChecked(true); + break; + case Message.ENCRYPTION_OTR: + otr.setChecked(true); + break; + case Message.ENCRYPTION_PGP: + popup.getMenu().findItem(R.id.encryption_choice_pgp) + .setChecked(true); + break; + default: + popup.getMenu().findItem(R.id.encryption_choice_none) + .setChecked(true); + break; + } + popup.show(); + } + } + + protected void muteConversationDialog(final Conversation conversation) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.disable_notifications_for_this_conversation); + final int[] durations = getResources().getIntArray( + R.array.mute_options_durations); + builder.setItems(R.array.mute_options_descriptions, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + long till; + if (durations[which] == -1) { + till = Long.MAX_VALUE; + } else { + till = SystemClock.elapsedRealtime() + + (durations[which] * 1000); + } + conversation.setMutedTill(till); + ConversationActivity.this.xmppConnectionService.databaseBackend + .updateConversation(conversation); + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.updateMessages(); + } + } + }); + builder.create().show(); + } + + protected ConversationFragment swapConversationFragment() { + ConversationFragment selectedFragment = new ConversationFragment(); + if (!isFinishing()) { + + FragmentTransaction transaction = getFragmentManager() + .beginTransaction(); + transaction.replace(R.id.selected_conversation, selectedFragment, + "conversation"); + try { + transaction.commitAllowingStateLoss(); + } catch (IllegalStateException e) { + return selectedFragment; + } + } + return selectedFragment; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (!isConversationsOverviewVisable()) { + showConversationsOverview(); + return false; + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onNewIntent(Intent intent) { + if (xmppConnectionServiceBound) { + if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION + .equals(intent.getType())))) { + String convToView = (String) intent.getExtras().get( + CONVERSATION); + updateConversationList(); + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(convToView)) { + setSelectedConversation(conversationList.get(i)); + break; + } + } + paneShouldBeOpen = false; + String text = intent.getExtras().getString(TEXT, null); + swapConversationFragment().setText(text); + } + } else { + handledViewIntent = false; + setIntent(intent); + } + } + + @Override + public void onStart() { + super.onStart(); + if (this.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + if (conversationList.size() >= 1) { + this.onConversationUpdate(); + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnConversationListChangedListener(); + xmppConnectionService.removeOnAccountListChangedListener(); + xmppConnectionService.removeOnRosterUpdateListener(); + xmppConnectionService.getNotificationService().setOpenConversation( + null); + } + super.onStop(); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + Conversation conversation = getSelectedConversation(); + if (conversation != null) { + savedInstanceState.putString(STATE_OPEN_CONVERSATION, + conversation.getUuid()); + } + savedInstanceState.putBoolean(STATE_PANEL_OPEN, + isConversationsOverviewVisable()); + super.onSaveInstanceState(savedInstanceState); + } + + @Override + void onBackendConnected() { + this.registerListener(); + updateConversationList(); + + if (xmppConnectionService.getAccounts().size() == 0) { + startActivity(new Intent(this, EditAccountActivity.class)); + } else if (conversationList.size() <= 0) { + startActivity(new Intent(this, StartConversationActivity.class)); + finish(); + } else if (mOpenConverstaion != null) { + selectConversationByUuid(mOpenConverstaion); + paneShouldBeOpen = mPanelOpen; + if (paneShouldBeOpen) { + showConversationsOverview(); + } + swapConversationFragment(); + mOpenConverstaion = null; + } else if (getIntent() != null + && VIEW_CONVERSATION.equals(getIntent().getType())) { + String uuid = (String) getIntent().getExtras().get(CONVERSATION); + String text = getIntent().getExtras().getString(TEXT, null); + selectConversationByUuid(uuid); + paneShouldBeOpen = false; + swapConversationFragment().setText(text); + setIntent(null); + } else { + showConversationsOverview(); + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.onBackendConnected(); + } else { + pendingImageUri = null; + setSelectedConversation(conversationList.get(0)); + swapConversationFragment(); + } + } + + if (pendingImageUri != null) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); + } + + private void selectConversationByUuid(String uuid) { + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(uuid)) { + setSelectedConversation(conversationList.get(i)); + } + } + } + + public void registerListener() { + xmppConnectionService.setOnConversationListChangedListener(this); + xmppConnectionService.setOnAccountListChangedListener(this); + xmppConnectionService.setOnRosterUpdateListener(this); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_DECRYPT_PGP) { + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.hideSnackbar(); + selectedFragment.updateMessages(); + } + } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { + pendingImageUri = data.getData(); + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + } else if (requestCode == REQUEST_SEND_PGP_IMAGE) { + + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + } else if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(getSelectedConversation().getAccount(), + getSelectedConversation()); + } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) { + // encryptTextMessage(); + } else if (requestCode == REQUEST_IMAGE_CAPTURE) { + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + Intent intent = new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(pendingImageUri); + sendBroadcast(intent); + } else if (requestCode == REQUEST_RECORD_AUDIO) { + attachAudioToConversation(getSelectedConversation(), + data.getData()); + } + } else { + if (requestCode == REQUEST_IMAGE_CAPTURE) { + pendingImageUri = null; + } + } + } + + private void attachAudioToConversation(Conversation conversation, Uri uri) { + + } + + private void attachImageToConversation(Conversation conversation, Uri uri) { + prepareImageToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), Toast.LENGTH_LONG); + prepareImageToast.show(); + xmppConnectionService.attachImageToConversation(conversation, uri, + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Message object) { + hidePrepareImageToast(); + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_PGP_IMAGE); + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + hidePrepareImageToast(); + displayErrorDialog(error); + } + }); + } + + private void hidePrepareImageToast() { + if (prepareImageToast != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + prepareImageToast.cancel(); + } + }); + } + } + + public void updateConversationList() { + xmppConnectionService + .populateWithOrderedConversations(conversationList); + listAdapter.notifyDataSetChanged(); + } + + public void runIntent(PendingIntent pi, int requestCode) { + try { + this.startIntentSenderForResult(pi.getIntentSender(), requestCode, + null, 0, 0, 0); + } catch (SendIntentException e1) { + } + } + + public void encryptTextMessage(Message message) { + xmppConnectionService.getPgpEngine().encrypt(message, + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Message message) { + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_MESSAGE); + } + + @Override + public void success(Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + + } + }); + } + + public boolean forceEncryption() { + return getPreferences().getBoolean("force_encryption", false); + } + + public boolean useSendButtonToIndicateStatus() { + return getPreferences().getBoolean("send_button_status", false); + } + + public boolean indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + @Override + public void onAccountUpdate() { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + fragment.updateMessages(); + } + }); + } + } + + @Override + public void onConversationUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + updateConversationList(); + if (paneShouldBeOpen) { + if (conversationList.size() >= 1) { + swapConversationFragment(); + } else { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + finish(); + } + } + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.updateMessages(); + } + } + }); + } + + @Override + public void onRosterUpdate() { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + fragment.updateMessages(); + } + }); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java new file mode 100644 index 000000000..0e71801bd --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -0,0 +1,781 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.EditMessage.OnEnterPressed; +import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; +import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; +import eu.siacs.conversations.ui.adapter.MessageAdapter; +import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; +import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.utils.UIHelper; +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; +import android.os.Bundle; +import android.text.Editable; +import android.text.Selection; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView.OnScrollListener; +import android.widget.TextView.OnEditorActionListener; +import android.widget.AbsListView; + +import android.widget.ListView; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class ConversationFragment extends Fragment { + + protected Conversation conversation; + protected ListView messagesView; + protected LayoutInflater inflater; + protected List messageList = new ArrayList(); + protected MessageAdapter messageListAdapter; + protected Contact contact; + + protected String queuedPqpMessage = null; + + private EditMessage mEditMessage; + private ImageButton mSendButton; + private String pastedText = null; + private RelativeLayout snackbar; + private TextView snackbarMessage; + private TextView snackbarAction; + + private boolean messagesLoaded = false; + + private IntentSender askForPassphraseIntent = null; + + private ConcurrentLinkedQueue mEncryptedMessages = new ConcurrentLinkedQueue(); + private boolean mDecryptJobRunning = false; + + private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() { + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEND) { + InputMethodManager imm = (InputMethodManager) v.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + sendMessage(); + return true; + } else { + return false; + } + } + }; + + private OnClickListener mSendButtonListener = new OnClickListener() { + + @Override + public void onClick(View v) { + sendMessage(); + } + }; + protected OnClickListener clickToDecryptListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (activity.hasPgp() && askForPassphraseIntent != null) { + try { + getActivity().startIntentSenderForResult( + askForPassphraseIntent, + ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, + 0, 0); + } catch (SendIntentException e) { + // + } + } + } + }; + + private 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); + } + }; + + private OnClickListener leaveMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + activity.endConversation(conversation); + } + }; + + private OnClickListener joinMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + activity.xmppConnectionService.joinMuc(conversation); + } + }; + + private OnClickListener enterPassword = new OnClickListener() { + + @Override + public void onClick(View v) { + MucOptions muc = conversation.getMucOptions(); + String password = muc.getPassword(); + if (password == null) { + password = ""; + } + activity.quickPasswordEdit(password, new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + activity.xmppConnectionService.providePasswordForMuc( + conversation, value); + } + }); + } + }; + + private OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // TODO Auto-generated method stub + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (firstVisibleItem == 0 && messagesLoaded) { + long timestamp = messageList.get(0).getTimeSent(); + messagesLoaded = false; + int size = activity.xmppConnectionService.loadMoreMessages( + conversation, timestamp); + messageList.clear(); + messageList.addAll(conversation.getMessages()); + updateStatusMessages(); + messageListAdapter.notifyDataSetChanged(); + if (size != 0) { + messagesLoaded = true; + } + messagesView.setSelectionFromTop(size + 1, 0); + } + } + }; + + private ConversationActivity activity; + + private void sendMessage() { + if (this.conversation == null) { + return; + } + if (mEditMessage.getText().length() < 1) { + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + updateChatMsgHint(); + } + return; + } + Message message = new Message(conversation, mEditMessage.getText() + .toString(), conversation.getNextEncryption(activity + .forceEncryption())); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getNextPresence() != null) { + message.setPresence(conversation.getNextPresence()); + message.setType(Message.TYPE_PRIVATE); + conversation.setNextPresence(null); + } + } + if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) { + sendOtrMessage(message); + } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { + sendPgpMessage(message); + } else { + sendPlainTextMessage(message); + } + } + + public void updateChatMsgHint() { + if (conversation.getMode() == Conversation.MODE_MULTI + && conversation.getNextPresence() != null) { + this.mEditMessage.setHint(getString( + R.string.send_private_message_to, + conversation.getNextPresence())); + } else { + switch (conversation.getNextEncryption(activity.forceEncryption())) { + case Message.ENCRYPTION_NONE: + mEditMessage + .setHint(getString(R.string.send_plain_text_message)); + break; + case Message.ENCRYPTION_OTR: + mEditMessage.setHint(getString(R.string.send_otr_message)); + break; + case Message.ENCRYPTION_PGP: + mEditMessage.setHint(getString(R.string.send_pgp_message)); + break; + default: + break; + } + } + } + + @Override + public View onCreateView(final LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.fragment_conversation, + container, false); + mEditMessage = (EditMessage) view.findViewById(R.id.textinput); + mEditMessage.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.hideConversationsOverview(); + } + }); + mEditMessage.setOnEditorActionListener(mEditorActionListener); + mEditMessage.setOnEnterPressedListener(new OnEnterPressed() { + + @Override + public void onEnterPressed() { + sendMessage(); + } + }); + + mSendButton = (ImageButton) view.findViewById(R.id.textSendButton); + mSendButton.setOnClickListener(this.mSendButtonListener); + + snackbar = (RelativeLayout) view.findViewById(R.id.snackbar); + snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message); + snackbarAction = (TextView) view.findViewById(R.id.snackbar_action); + + messagesView = (ListView) view.findViewById(R.id.messages_view); + messagesView.setOnScrollListener(mOnScrollListener); + messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); + messageListAdapter = new MessageAdapter( + (ConversationActivity) getActivity(), this.messageList); + messageListAdapter + .setOnContactPictureClicked(new OnContactPictureClicked() { + + @Override + public void onContactPictureClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getPresence() != null) { + highlightInConference(message.getPresence()); + } else { + highlightInConference(message + .getCounterpart()); + } + } else { + Contact contact = message.getConversation() + .getContact(); + if (contact.showInRoster()) { + activity.switchToContactDetails(contact); + } else { + activity.showAddToRosterDialog(message + .getConversation()); + } + } + } + } + }); + messageListAdapter + .setOnContactPictureLongClicked(new OnContactPictureLongClicked() { + + @Override + public void onContactPictureLongClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getPresence() != null) { + privateMessageWith(message.getPresence()); + } else { + privateMessageWith(message.getCounterpart()); + } + } + } + } + }); + messagesView.setAdapter(messageListAdapter); + + return view; + } + + protected void privateMessageWith(String counterpart) { + this.mEditMessage.setText(""); + this.conversation.setNextPresence(counterpart); + updateChatMsgHint(); + } + + protected void highlightInConference(String nick) { + String oldString = mEditMessage.getText().toString().trim(); + if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) { + mEditMessage.getText().insert(0, nick + ": "); + } else { + if (mEditMessage.getText().charAt( + mEditMessage.getSelectionStart() - 1) != ' ') { + nick = " " + nick; + } + mEditMessage.getText().insert(mEditMessage.getSelectionStart(), + nick + " "); + } + } + + @Override + public void onStart() { + super.onStart(); + this.activity = (ConversationActivity) getActivity(); + if (activity.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + } + + @Override + public void onStop() { + mDecryptJobRunning = false; + super.onStop(); + if (this.conversation != null) { + this.conversation.setNextMessage(mEditMessage.getText().toString()); + } + } + + public void onBackendConnected() { + this.activity = (ConversationActivity) getActivity(); + this.conversation = activity.getSelectedConversation(); + if (this.conversation == null) { + return; + } + String oldString = conversation.getNextMessage().trim(); + if (this.pastedText == null) { + this.mEditMessage.setText(oldString); + } else { + + if (oldString.isEmpty()) { + mEditMessage.setText(pastedText); + } else { + mEditMessage.setText(oldString + " " + pastedText); + } + pastedText = null; + } + int position = mEditMessage.length(); + Editable etext = mEditMessage.getText(); + Selection.setSelection(etext, position); + if (activity.isConversationsOverviewHideable()) { + if (!activity.shouldPaneBeOpen()) { + activity.hideConversationsOverview(); + activity.openConversation(conversation); + } + } + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + } + updateMessages(); + } + + public void updateMessages() { + if (getView() == null) { + return; + } + hideSnackbar(); + final ConversationActivity activity = (ConversationActivity) getActivity(); + if (this.conversation != null) { + final Contact contact = this.conversation.getContact(); + if (this.conversation.isMuted()) { + showSnackbar(R.string.notifications_disabled, R.string.enable, + new OnClickListener() { + + @Override + public void onClick(View v) { + conversation.setMutedTill(0); + activity.xmppConnectionService.databaseBackend + .updateConversation(conversation); + updateMessages(); + } + }); + } else if (!contact.showInRoster() + && contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar(R.string.contact_added_you, R.string.add_back, + new OnClickListener() { + + @Override + public void onClick(View v) { + activity.xmppConnectionService + .createContact(contact); + activity.switchToContactDetails(contact); + } + }); + } + for (Message message : this.conversation.getMessages()) { + if ((message.getEncryption() == Message.ENCRYPTION_PGP) + && ((message.getStatus() == Message.STATUS_RECEIVED) || (message + .getStatus() == Message.STATUS_SEND))) { + if (!mEncryptedMessages.contains(message)) { + mEncryptedMessages.add(message); + } + } + } + decryptNext(); + this.messageList.clear(); + if (this.conversation.getMessages().size() == 0) { + messagesLoaded = false; + } else { + this.messageList.addAll(this.conversation.getMessages()); + messagesLoaded = true; + updateStatusMessages(); + } + this.messageListAdapter.notifyDataSetChanged(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (messageList.size() >= 1) { + makeFingerprintWarning(conversation.getLatestEncryption()); + } + } else { + if (!conversation.getMucOptions().online() + && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + int error = conversation.getMucOptions().getError(); + switch (error) { + case MucOptions.ERROR_NICK_IN_USE: + showSnackbar(R.string.nick_in_use, R.string.edit, + clickToMuc); + break; + case MucOptions.ERROR_ROOM_NOT_FOUND: + showSnackbar(R.string.conference_not_found, + R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_PASSWORD_REQUIRED: + showSnackbar(R.string.conference_requires_password, + R.string.enter_password, enterPassword); + break; + case MucOptions.ERROR_BANNED: + showSnackbar(R.string.conference_banned, + R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_MEMBERS_ONLY: + showSnackbar(R.string.conference_members_only, + R.string.leave, leaveMuc); + break; + case MucOptions.KICKED_FROM_ROOM: + showSnackbar(R.string.conference_kicked, R.string.join, + joinMuc); + break; + default: + break; + } + } + } + getActivity().invalidateOptionsMenu(); + updateChatMsgHint(); + if (!activity.shouldPaneBeOpen()) { + activity.xmppConnectionService.markRead(conversation, true); + activity.updateConversationList(); + } + this.updateSendButton(); + } + } + + private void decryptNext() { + Message next = this.mEncryptedMessages.peek(); + PgpEngine engine = activity.xmppConnectionService.getPgpEngine(); + + if (next != null && engine != null && !mDecryptJobRunning) { + mDecryptJobRunning = true; + engine.decrypt(next, new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + mDecryptJobRunning = false; + askForPassphraseIntent = pi.getIntentSender(); + showSnackbar(R.string.openpgp_messages_found, + R.string.decrypt, clickToDecryptListener); + } + + @Override + public void success(Message message) { + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateMessage(message); + } + + @Override + public void error(int error, Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateConversationUi(); + } + }); + } + } + + private void messageSent() { + int size = this.messageList.size(); + messagesView.setSelection(size - 1); + mEditMessage.setText(""); + updateChatMsgHint(); + } + + public void updateSendButton() { + Conversation c = this.conversation; + if (activity.useSendButtonToIndicateStatus() && c != null + && c.getAccount().getStatus() == Account.STATUS_ONLINE) { + if (c.getMode() == Conversation.MODE_SINGLE) { + switch (c.getContact().getMostAvailableStatus()) { + case Presences.CHAT: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + break; + case Presences.ONLINE: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + break; + case Presences.AWAY: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_away); + break; + case Presences.XA: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_away); + break; + case Presences.DND: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_dnd); + break; + default: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + break; + } + } else if (c.getMode() == Conversation.MODE_MULTI) { + if (c.getMucOptions().online()) { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } + + protected void updateStatusMessages() { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + for (int i = this.messageList.size() - 1; i >= 0; --i) { + if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { + return; + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + this.messageList.add(i + 1, + Message.createStatusMessage(conversation)); + return; + } + } + } + } + } + + protected void makeFingerprintWarning(int latestEncryption) { + Set knownFingerprints = conversation.getContact() + .getOtrFingerprints(); + if ((latestEncryption == Message.ENCRYPTION_OTR) + && (conversation.hasValidOtrSession() + && (!conversation.isMuted()) + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints + .contains(conversation.getOtrFingerprint())))) { + showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, + new OnClickListener() { + + @Override + public void onClick(View v) { + if (conversation.getOtrFingerprint() != null) { + AlertDialog dialog = UIHelper + .getVerifyFingerprintDialog( + (ConversationActivity) getActivity(), + conversation, snackbar); + dialog.show(); + } + } + }); + } + } + + protected void showSnackbar(int message, int action, + OnClickListener clickListener) { + snackbar.setVisibility(View.VISIBLE); + snackbar.setOnClickListener(null); + snackbarMessage.setText(message); + snackbarMessage.setOnClickListener(null); + snackbarAction.setText(action); + snackbarAction.setOnClickListener(clickListener); + } + + protected void hideSnackbar() { + snackbar.setVisibility(View.GONE); + } + + protected void sendPlainTextMessage(Message message) { + ConversationActivity activity = (ConversationActivity) getActivity(); + activity.xmppConnectionService.sendMessage(message); + messageSent(); + } + + protected void sendPgpMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + final Contact contact = message.getConversation().getContact(); + if (activity.hasPgp()) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (contact.getPgpKeyId() != 0) { + xmppService.getPgpEngine().hasKey(contact, + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + activity.runIntent( + pi, + ConversationActivity.REQUEST_ENCRYPT_MESSAGE); + } + + @Override + public void success(Contact contact) { + messageSent(); + activity.encryptTextMessage(message); + } + + @Override + public void error(int error, Contact contact) { + + } + }); + + } else { + showNoPGPKeyDialog(false, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + xmppService.databaseBackend + .updateConversation(conversation); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } else { + if (conversation.getMucOptions().pgpKeysInUse()) { + if (!conversation.getMucOptions().everybodyHasKeys()) { + Toast warning = Toast + .makeText(getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); + warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); + warning.show(); + } + activity.encryptTextMessage(message); + messageSent(); + } else { + showNoPGPKeyDialog(true, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.databaseBackend + .updateConversation(conversation); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } + } else { + activity.showInstallPgpDialog(); + } + } + + public void showNoPGPKeyDialog(boolean plural, + DialogInterface.OnClickListener listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + if (plural) { + builder.setTitle(getString(R.string.no_pgp_keys)); + builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); + } else { + builder.setTitle(getString(R.string.no_pgp_key)); + builder.setMessage(getText(R.string.contact_has_no_pgp_key)); + } + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.send_unencrypted), + listener); + builder.create().show(); + } + + protected void sendOtrMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + if (conversation.hasValidOtrSession()) { + activity.xmppConnectionService.sendMessage(message); + messageSent(); + } else { + activity.selectPresence(message.getConversation(), + new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + message.setPresence(conversation.getNextPresence()); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } + + public void setText(String text) { + this.pastedText = text; + } + + public void clearInputField() { + this.mEditMessage.setText(""); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java new file mode 100644 index 000000000..1543d7402 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -0,0 +1,423 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.utils.Validator; +import eu.siacs.conversations.xmpp.XmppConnection.Features; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class EditAccountActivity extends XmppActivity { + + private AutoCompleteTextView mAccountJid; + private EditText mPassword; + private EditText mPasswordConfirm; + private CheckBox mRegisterNew; + private Button mCancelButton; + private Button mSaveButton; + + private LinearLayout mStats; + private TextView mServerInfoSm; + private TextView mServerInfoCarbons; + private TextView mServerInfoPep; + private TextView mSessionEst; + private TextView mOtrFingerprint; + private RelativeLayout mOtrFingerprintBox; + private ImageButton mOtrFingerprintToClipboardButton; + + private String jidToEdit; + private Account mAccount; + + private boolean mFetchingAvatar = false; + + private OnClickListener mSaveButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_DISABLED) { + mAccount.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(mAccount); + return; + } + if (!Validator.isValidJid(mAccountJid.getText().toString())) { + mAccountJid.setError(getString(R.string.invalid_jid)); + mAccountJid.requestFocus(); + return; + } + boolean registerNewAccount = mRegisterNew.isChecked(); + String[] jidParts = mAccountJid.getText().toString().split("@"); + String username = jidParts[0]; + String server; + if (jidParts.length >= 2) { + server = jidParts[1]; + } else { + server = ""; + } + String password = mPassword.getText().toString(); + String passwordConfirm = mPasswordConfirm.getText().toString(); + if (registerNewAccount) { + if (!password.equals(passwordConfirm)) { + mPasswordConfirm + .setError(getString(R.string.passwords_do_not_match)); + mPasswordConfirm.requestFocus(); + return; + } + } + if (mAccount != null) { + mAccount.setPassword(password); + mAccount.setUsername(username); + mAccount.setServer(server); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.updateAccount(mAccount); + } else { + if (xmppConnectionService.findAccountByJid(mAccountJid + .getText().toString()) != null) { + mAccountJid + .setError(getString(R.string.account_already_exists)); + mAccountJid.requestFocus(); + return; + } + mAccount = new Account(username, server, password); + mAccount.setOption(Account.OPTION_USETLS, true); + mAccount.setOption(Account.OPTION_USECOMPRESSION, true); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.createAccount(mAccount); + } + if (jidToEdit != null) { + finish(); + } else { + updateSaveButton(); + updateAccountInformation(); + } + + } + }; + private OnClickListener mCancelButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + finish(); + } + }; + private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mAccount != null + && mAccount.getStatus() != Account.STATUS_ONLINE + && mFetchingAvatar) { + startActivity(new Intent(getApplicationContext(), + ManageAccountActivity.class)); + finish(); + } else if (jidToEdit == null && mAccount != null + && mAccount.getStatus() == Account.STATUS_ONLINE) { + if (!mFetchingAvatar) { + mFetchingAvatar = true; + xmppConnectionService.checkForAvatar(mAccount, + mAvatarFetchCallback); + } + } else { + updateSaveButton(); + } + if (mAccount != null) { + updateAccountInformation(); + } + } + }); + } + }; + private UiCallback mAvatarFetchCallback = new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void success(Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void error(int errorCode, Avatar avatar) { + finishInitialSetup(avatar); + } + }; + private KnownHostsAdapter mKnownHostsAdapter; + private TextWatcher mTextWatcher = new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + updateSaveButton(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }; + + protected void finishInitialSetup(final Avatar avatar) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + Intent intent; + if (avatar != null) { + intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + } else { + intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", mAccount.getJid()); + intent.putExtra("setup", true); + } + startActivity(intent); + finish(); + } + }); + } + + protected boolean inputDataDiffersFromAccount() { + if (mAccount == null) { + return true; + } else { + return (!mAccount.getJid().equals(mAccountJid.getText().toString())) + || (!mAccount.getPassword().equals( + mPassword.getText().toString()) || mAccount + .isOptionSet(Account.OPTION_REGISTER) != mRegisterNew + .isChecked()); + } + } + + protected void updateSaveButton() { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_CONNECTING) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + this.mSaveButton.setText(R.string.account_status_connecting); + } else if (mAccount != null + && mAccount.getStatus() == Account.STATUS_DISABLED) { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + this.mSaveButton.setText(R.string.enable); + } else { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + if (jidToEdit != null) { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_ONLINE) { + this.mSaveButton.setText(R.string.save); + if (!accountInfoEdited()) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + } + } else { + this.mSaveButton.setText(R.string.connect); + } + } else { + this.mSaveButton.setText(R.string.next); + } + } + } + + protected boolean accountInfoEdited() { + return (!this.mAccount.getJid().equals( + this.mAccountJid.getText().toString())) + || (!this.mAccount.getPassword().equals( + this.mPassword.getText().toString())); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_account); + this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid); + this.mAccountJid.addTextChangedListener(this.mTextWatcher); + this.mPassword = (EditText) findViewById(R.id.account_password); + this.mPassword.addTextChangedListener(this.mTextWatcher); + this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm); + this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new); + this.mStats = (LinearLayout) findViewById(R.id.stats); + this.mSessionEst = (TextView) findViewById(R.id.session_est); + this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); + this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); + this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); + this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); + this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); + this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mSaveButton = (Button) findViewById(R.id.save_button); + this.mCancelButton = (Button) findViewById(R.id.cancel_button); + this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); + this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener); + this.mRegisterNew + .setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + mPasswordConfirm.setVisibility(View.VISIBLE); + } else { + mPasswordConfirm.setVisibility(View.GONE); + } + updateSaveButton(); + } + }); + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + this.jidToEdit = getIntent().getStringExtra("jid"); + if (this.jidToEdit != null) { + this.mRegisterNew.setVisibility(View.GONE); + getActionBar().setTitle(jidToEdit); + } else { + getActionBar().setTitle(R.string.action_add_account); + } + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnAccountListChangedListener(); + } + super.onStop(); + } + + @Override + protected void onBackendConnected() { + this.mKnownHostsAdapter = new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, + xmppConnectionService.getKnownHosts()); + this.xmppConnectionService + .setOnAccountListChangedListener(this.mOnAccountUpdateListener); + if (this.jidToEdit != null) { + this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); + updateAccountInformation(); + } else if (this.xmppConnectionService.getAccounts().size() == 0) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setDisplayShowHomeEnabled(false); + this.mCancelButton.setEnabled(false); + this.mCancelButton.setTextColor(getSecondaryTextColor()); + } + this.mAccountJid.setAdapter(this.mKnownHostsAdapter); + updateSaveButton(); + } + + private void updateAccountInformation() { + this.mAccountJid.setText(this.mAccount.getJid()); + this.mPassword.setText(this.mAccount.getPassword()); + if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { + this.mRegisterNew.setVisibility(View.VISIBLE); + this.mRegisterNew.setChecked(true); + this.mPasswordConfirm.setText(this.mAccount.getPassword()); + } else { + this.mRegisterNew.setVisibility(View.GONE); + this.mRegisterNew.setChecked(false); + } + if (this.mAccount.getStatus() == Account.STATUS_ONLINE + && !this.mFetchingAvatar) { + this.mStats.setVisibility(View.VISIBLE); + this.mSessionEst.setText(UIHelper.readableTimeDifference( + getApplicationContext(), this.mAccount.getXmppConnection() + .getLastSessionEstablished())); + Features features = this.mAccount.getXmppConnection().getFeatures(); + if (features.carbons()) { + this.mServerInfoCarbons.setText(R.string.server_info_available); + } else { + this.mServerInfoCarbons + .setText(R.string.server_info_unavailable); + } + if (features.sm()) { + this.mServerInfoSm.setText(R.string.server_info_available); + } else { + this.mServerInfoSm.setText(R.string.server_info_unavailable); + } + if (features.pubsub()) { + this.mServerInfoPep.setText(R.string.server_info_available); + } else { + this.mServerInfoPep.setText(R.string.server_info_unavailable); + } + final String fingerprint = this.mAccount + .getOtrFingerprint(xmppConnectionService); + if (fingerprint != null) { + this.mOtrFingerprintBox.setVisibility(View.VISIBLE); + this.mOtrFingerprint.setText(fingerprint); + this.mOtrFingerprintToClipboardButton + .setVisibility(View.VISIBLE); + this.mOtrFingerprintToClipboardButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + + if (OtrFingerprintToClipBoard(fingerprint)) { + Toast.makeText( + EditAccountActivity.this, + R.string.toast_message_otr_fingerprint, + Toast.LENGTH_SHORT).show(); + } + } + }); + } else { + this.mOtrFingerprintBox.setVisibility(View.GONE); + } + } else { + if (this.mAccount.errorStatus()) { + this.mAccountJid.setError(getString(this.mAccount + .getReadableStatusId())); + this.mAccountJid.requestFocus(); + } + this.mStats.setVisibility(View.GONE); + } + } + + private boolean OtrFingerprintToClipBoard(String fingerprint) { + ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String label = getResources().getString(R.string.otr_fingerprint); + if (mClipBoardManager != null) { + ClipData mClipData = ClipData.newPlainText(label, fingerprint); + mClipBoardManager.setPrimaryClip(mClipData); + return true; + } + return false; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java new file mode 100644 index 000000000..f83020506 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -0,0 +1,39 @@ +package eu.siacs.conversations.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + +public class EditMessage extends EditText { + + public EditMessage(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditMessage(Context context) { + super(context); + } + + protected OnEnterPressed mOnEnterPressed; + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mOnEnterPressed != null) { + mOnEnterPressed.onEnterPressed(); + } + return true; + } + return super.onKeyDown(keyCode, event); + } + + public void setOnEnterPressedListener(OnEnterPressed listener) { + this.mOnEnterPressed = listener; + } + + public interface OnEnterPressed { + public void onEnterPressed(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java new file mode 100644 index 000000000..5b5b0608f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -0,0 +1,217 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.AccountAdapter; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +public class ManageAccountActivity extends XmppActivity { + + protected Account selectedAccount = null; + + protected List accountList = new ArrayList(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + protected OnAccountUpdate accountChanged = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + runOnUiThread(new Runnable() { + + @Override + public void run() { + mAccountAdapter.notifyDataSetChanged(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.manage_accounts); + + accountListView = (ListView) findViewById(R.id.account_list); + this.mAccountAdapter = new AccountAdapter(this, accountList); + accountListView.setAdapter(this.mAccountAdapter); + accountListView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View view, + int position, long arg3) { + switchToAccount(accountList.get(position)); + } + }); + registerForContextMenu(accountListView); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + ManageAccountActivity.this.getMenuInflater().inflate( + R.menu.manageaccounts_context, menu); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedAccount = accountList.get(acmi.position); + if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) { + menu.findItem(R.id.mgmt_account_disable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false); + menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); + } else { + menu.findItem(R.id.mgmt_account_enable).setVisible(false); + } + menu.setHeaderTitle(this.selectedAccount.getJid()); + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnAccountListChangedListener(); + } + super.onStop(); + } + + @Override + void onBackendConnected() { + xmppConnectionService.setOnAccountListChangedListener(accountChanged); + this.accountList.clear(); + this.accountList.addAll(xmppConnectionService.getAccounts()); + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manageaccounts, menu); + return true; + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.mgmt_account_publish_avatar: + publishAvatar(selectedAccount); + return true; + case R.id.mgmt_account_disable: + disableAccount(selectedAccount); + return true; + case R.id.mgmt_account_enable: + enableAccount(selectedAccount); + return true; + case R.id.mgmt_account_delete: + deleteAccount(selectedAccount); + return true; + case R.id.mgmt_account_announce_pgp: + publishOpenPGPPublicKey(selectedAccount); + default: + return super.onContextItemSelected(item); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add_account: + startActivity(new Intent(getApplicationContext(), + EditAccountActivity.class)); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onNavigateUp() { + if (xmppConnectionService.getConversations().size() == 0) { + Intent contactsIntent = new Intent(this, + StartConversationActivity.class); + contactsIntent.setFlags( + // if activity exists in stack, pop the stack and go back to it + Intent.FLAG_ACTIVITY_CLEAR_TOP | + // otherwise, make a new task for it + Intent.FLAG_ACTIVITY_NEW_TASK | + // don't use the new activity animation; finish + // animation runs instead + Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(contactsIntent); + finish(); + return true; + } else { + return super.onNavigateUp(); + } + } + + private void publishAvatar(Account account) { + Intent intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", account.getJid()); + startActivity(intent); + } + + private void disableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, true); + xmppConnectionService.updateAccount(account); + } + + private void enableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(account); + } + + private void publishOpenPGPPublicKey(Account account) { + if (ManageAccountActivity.this.hasPgp()) { + announcePgp(account, null); + } else { + this.showInstallPgpDialog(); + } + } + + private void deleteAccount(final Account account) { + AlertDialog.Builder builder = new AlertDialog.Builder( + ManageAccountActivity.this); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteAccount(account); + selectedAccount = null; + } + }); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.create().show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(selectedAccount, null); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java new file mode 100644 index 000000000..6aa40c418 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -0,0 +1,242 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class PublishProfilePictureActivity extends XmppActivity { + + private static final int REQUEST_CHOOSE_FILE = 0xac23; + + private ImageView avatar; + private TextView accountTextView; + private TextView hintOrWarning; + private TextView secondaryHint; + private Button cancelButton; + private Button publishButton; + + private Uri avatarUri; + private Uri defaultUri; + + private Account account; + + private boolean support = false; + + private boolean mInitialAccountSetup; + + private UiCallback avatarPublication = new UiCallback() { + + @Override + public void success(Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mInitialAccountSetup) { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + } + finish(); + } + }); + } + + @Override + public void error(final int errorCode, Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + hintOrWarning.setText(errorCode); + hintOrWarning.setTextColor(getWarningTextColor()); + publishButton.setText(R.string.publish); + enablePublishButton(); + } + }); + + } + + @Override + public void userInputRequried(PendingIntent pi, Avatar object) { + } + }; + + private OnLongClickListener backToDefaultListener = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + avatarUri = defaultUri; + loadImageIntoPreview(defaultUri); + return true; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_publish_profile_picture); + this.avatar = (ImageView) findViewById(R.id.account_image); + this.cancelButton = (Button) findViewById(R.id.cancel_button); + this.publishButton = (Button) findViewById(R.id.publish_button); + this.accountTextView = (TextView) findViewById(R.id.account); + this.hintOrWarning = (TextView) findViewById(R.id.hint_or_warning); + this.secondaryHint = (TextView) findViewById(R.id.secondary_hint); + this.publishButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (avatarUri != null) { + publishButton.setText(R.string.publishing); + disablePublishButton(); + xmppConnectionService.publishAvatar(account, avatarUri, + avatarPublication); + } + } + }); + this.cancelButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mInitialAccountSetup) { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + } + finish(); + } + }); + this.avatar.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_CHOOSE_FILE); + } + }); + this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext()); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_CHOOSE_FILE) { + this.avatarUri = data.getData(); + if (xmppConnectionServiceBound) { + loadImageIntoPreview(this.avatarUri); + } + } + } + } + + @Override + protected void onBackendConnected() { + if (getIntent() != null) { + String jid = getIntent().getStringExtra("account"); + if (jid != null) { + this.account = xmppConnectionService.findAccountByJid(jid); + if (this.account.getXmppConnection() != null) { + this.support = this.account.getXmppConnection() + .getFeatures().pubsub(); + } + if (this.avatarUri == null) { + if (this.account.getAvatar() != null + || this.defaultUri == null) { + this.avatar.setImageBitmap(avatarService().get(account, + getPixel(194))); + if (this.defaultUri != null) { + this.avatar + .setOnLongClickListener(this.backToDefaultListener); + } else { + this.secondaryHint.setVisibility(View.INVISIBLE); + } + if (!support) { + this.hintOrWarning + .setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_no_server_support); + } + } else { + this.avatarUri = this.defaultUri; + loadImageIntoPreview(this.defaultUri); + this.secondaryHint.setVisibility(View.INVISIBLE); + } + } else { + loadImageIntoPreview(avatarUri); + } + this.accountTextView.setText(this.account.getJid()); + } + } + + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", + false); + } + if (this.mInitialAccountSetup) { + this.cancelButton.setText(R.string.skip); + } + } + + protected void loadImageIntoPreview(Uri uri) { + Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare( + uri, 384); + if (bm == null) { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_converting); + return; + } + this.avatar.setImageBitmap(bm); + if (support) { + enablePublishButton(); + this.publishButton.setText(R.string.publish); + this.hintOrWarning.setText(R.string.publish_avatar_explanation); + this.hintOrWarning.setTextColor(getPrimaryTextColor()); + } else { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_no_server_support); + } + if (this.defaultUri != null && uri.equals(this.defaultUri)) { + this.secondaryHint.setVisibility(View.INVISIBLE); + this.avatar.setOnLongClickListener(null); + } else if (this.defaultUri != null) { + this.secondaryHint.setVisibility(View.VISIBLE); + this.avatar.setOnLongClickListener(this.backToDefaultListener); + } + } + + protected void enablePublishButton() { + this.publishButton.setEnabled(true); + this.publishButton.setTextColor(getPrimaryTextColor()); + } + + protected void disablePublishButton() { + this.publishButton.setEnabled(false); + this.publishButton.setTextColor(getSecondaryTextColor()); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java new file mode 100644 index 000000000..fc6308fce --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import eu.siacs.conversations.entities.Account; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Build; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.PreferenceManager; + +public class SettingsActivity extends XmppActivity implements + OnSharedPreferenceChangeListener { + private SettingsFragment mSettingsFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSettingsFragment = new SettingsFragment(); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, mSettingsFragment).commit(); + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this); + ListPreference resources = (ListPreference) mSettingsFragment + .findPreference("resource"); + if (resources != null) { + ArrayList entries = new ArrayList( + Arrays.asList(resources.getEntries())); + entries.add(0, Build.MODEL); + resources.setEntries(entries.toArray(new CharSequence[entries + .size()])); + resources.setEntryValues(entries.toArray(new CharSequence[entries + .size()])); + } + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, + String name) { + if (name.equals("resource")) { + String resource = preferences.getString("resource", "mobile") + .toLowerCase(Locale.US); + if (xmppConnectionServiceBound) { + for (Account account : xmppConnectionService.getAccounts()) { + account.setResource(resource); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + xmppConnectionService.reconnectAccount(account, false); + } + } + } + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java new file mode 100644 index 000000000..7e1c36989 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.ui; + +import eu.siacs.conversations.R; +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class SettingsFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java new file mode 100644 index 000000000..9fbc3db10 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -0,0 +1,185 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.Toast; + +public class ShareWithActivity extends XmppActivity { + + private class Share { + public Uri uri; + public String account; + public String contact; + public String text; + } + + private Share share; + + private static final int REQUEST_START_NEW_CONVERSATION = 0x0501; + private ListView mListView; + private List mConversations = new ArrayList(); + + private UiCallback attachImageCallback = new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, Message object) { + // TODO Auto-generated method stub + + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message object) { + // TODO Auto-generated method stub + + } + }; + + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_START_NEW_CONVERSATION + && resultCode == RESULT_OK) { + share.contact = data.getStringExtra("contact"); + share.account = data.getStringExtra("account"); + Log.d(Config.LOGTAG, "contact: " + share.contact + " account:" + + share.account); + } + if (xmppConnectionServiceBound && share != null + && share.contact != null && share.account != null) { + share(); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + + setContentView(R.layout.share_with); + setTitle(getString(R.string.title_activity_sharewith)); + + mListView = (ListView) findViewById(R.id.choose_conversation_list); + ConversationAdapter mAdapter = new ConversationAdapter(this, + this.mConversations); + mListView.setAdapter(mAdapter); + mListView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + Conversation conversation = mConversations.get(position); + if (conversation.getMode() == Conversation.MODE_SINGLE + || share.uri == null) { + share(mConversations.get(position)); + } + } + }); + + this.share = new Share(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.share_with, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add: + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onStart() { + if (getIntent().getType() != null + && getIntent().getType().startsWith("image/")) { + this.share.uri = (Uri) getIntent().getParcelableExtra( + Intent.EXTRA_STREAM); + } else { + this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT); + } + if (xmppConnectionServiceBound) { + xmppConnectionService.populateWithOrderedConversations( + mConversations, this.share.uri == null); + } + super.onStart(); + } + + @Override + void onBackendConnected() { + if (xmppConnectionServiceBound && share != null + && share.contact != null && share.account != null) { + share(); + return; + } + xmppConnectionService.populateWithOrderedConversations(mConversations, + this.share != null && this.share.uri == null); + } + + private void share() { + Account account = xmppConnectionService.findAccountByJid(share.account); + if (account == null) { + return; + } + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, share.contact, false); + share(conversation); + } + + private void share(final Conversation conversation) { + if (share.uri != null) { + selectPresence(conversation, new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), + Toast.LENGTH_LONG).show(); + ShareWithActivity.this.xmppConnectionService + .attachImageToConversation(conversation, share.uri, + attachImageCallback); + switchToConversation(conversation, null, true); + finish(); + } + }); + + } else { + switchToConversation(conversation, this.share.text, true); + finish(); + } + + } + +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java new file mode 100644 index 000000000..a1a2d4c2a --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -0,0 +1,677 @@ +package eu.siacs.conversations.ui; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.app.ActionBar.Tab; +import android.app.ActionBar.TabListener; +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.ListFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Spinner; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.ui.adapter.ListItemAdapter; +import eu.siacs.conversations.utils.Validator; + +public class StartConversationActivity extends XmppActivity { + + private Tab mContactsTab; + private Tab mConferencesTab; + private ViewPager mViewPager; + + private MyListFragment mContactsListFragment = new MyListFragment(); + private List contacts = new ArrayList(); + private ArrayAdapter mContactsAdapter; + + private MyListFragment mConferenceListFragment = new MyListFragment(); + private List conferences = new ArrayList(); + private ArrayAdapter mConferenceAdapter; + + private List mActivatedAccounts = new ArrayList(); + private List mKnownHosts; + private List mKnownConferenceHosts; + + private Menu mOptionsMenu; + private EditText mSearchEditText; + + public int conference_context_id; + public int contact_context_id; + + private TabListener mTabListener = new TabListener() { + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + return; + } + + @Override + public void onTabSelected(Tab tab, FragmentTransaction ft) { + mViewPager.setCurrentItem(tab.getPosition()); + onTabChanged(); + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + return; + } + }; + + private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + getActionBar().setSelectedNavigationItem(position); + onTabChanged(); + } + }; + + private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filter(null); + return true; + } + }; + private TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable editable) { + filter(editable.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + }; + private OnRosterUpdate onRosterUpdate = new OnRosterUpdate() { + + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } + } + }); + } + }; + private MenuItem mMenuSearchView; + private String mInitialJid; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_start_conversation); + mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager); + ActionBar actionBar = getActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + + mContactsTab = actionBar.newTab().setText(R.string.contacts) + .setTabListener(mTabListener); + mConferencesTab = actionBar.newTab().setText(R.string.conferences) + .setTabListener(mTabListener); + actionBar.addTab(mContactsTab); + actionBar.addTab(mConferencesTab); + + mViewPager.setOnPageChangeListener(mOnPageChangeListener); + mViewPager.setAdapter(new FragmentPagerAdapter(getFragmentManager()) { + + @Override + public int getCount() { + return 2; + } + + @Override + public Fragment getItem(int position) { + if (position == 0) { + return mContactsListFragment; + } else { + return mConferenceListFragment; + } + } + }); + + mConferenceAdapter = new ListItemAdapter(this, conferences); + mConferenceListFragment.setListAdapter(mConferenceAdapter); + mConferenceListFragment.setContextMenu(R.menu.conference_context); + mConferenceListFragment + .setOnListItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + openConversationForBookmark(position); + } + }); + + mContactsAdapter = new ListItemAdapter(this, contacts); + mContactsListFragment.setListAdapter(mContactsAdapter); + mContactsListFragment.setContextMenu(R.menu.contact_context); + mContactsListFragment + .setOnListItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + openConversationForContact(position); + } + }); + + } + + @Override + public void onStop() { + super.onStop(); + xmppConnectionService.removeOnRosterUpdateListener(); + } + + protected void openConversationForContact(int position) { + Contact contact = (Contact) contacts.get(position); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); + switchToConversation(conversation); + } + + protected void openConversationForContact() { + int position = contact_context_id; + openConversationForContact(position); + } + + protected void openConversationForBookmark() { + openConversationForBookmark(conference_context_id); + } + + protected void openConversationForBookmark(int position) { + Bookmark bookmark = (Bookmark) conferences.get(position); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(bookmark.getAccount(), + bookmark.getJid(), true); + conversation.setBookmark(bookmark); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + if (!bookmark.autojoin()) { + bookmark.setAutojoin(true); + xmppConnectionService.pushBookmarks(bookmark.getAccount()); + } + switchToConversation(conversation); + } + + protected void openDetailsForContact() { + int position = contact_context_id; + Contact contact = (Contact) contacts.get(position); + switchToContactDetails(contact); + } + + protected void deleteContact() { + int position = contact_context_id; + final Contact contact = (Contact) contacts.get(position); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(R.string.cancel, null); + builder.setTitle(R.string.action_delete_contact); + builder.setMessage(getString(R.string.remove_contact_text, + contact.getJid())); + builder.setPositiveButton(R.string.delete, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteContactOnServer(contact); + filter(mSearchEditText.getText().toString()); + } + }); + builder.create().show(); + + } + + protected void deleteConference() { + int position = conference_context_id; + final Bookmark bookmark = (Bookmark) conferences.get(position); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(R.string.cancel, null); + builder.setTitle(R.string.delete_bookmark); + builder.setMessage(getString(R.string.remove_bookmark_text, + bookmark.getJid())); + builder.setPositiveButton(R.string.delete, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + bookmark.unregisterConversation(); + Account account = bookmark.getAccount(); + account.getBookmarks().remove(bookmark); + xmppConnectionService.pushBookmarks(account); + filter(mSearchEditText.getText().toString()); + } + }); + builder.create().show(); + + } + + @SuppressLint("InflateParams") + protected void showCreateContactDialog(String prefilledJid) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.create_contact); + View dialogView = getLayoutInflater().inflate( + R.layout.create_contact_dialog, null); + final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView + .findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, mKnownHosts)); + if (prefilledJid != null) { + jid.append(prefilledJid); + } + populateAccountSpinner(spinner); + builder.setView(dialogView); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.create, null); + final AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener( + new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (!xmppConnectionServiceBound) { + return; + } + if (Validator.isValidJid(jid.getText().toString())) { + String accountJid = (String) spinner + .getSelectedItem(); + String contactJid = jid.getText().toString(); + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + Contact contact = account.getRoster().getContact( + contactJid); + if (contact.showInRoster()) { + jid.setError(getString(R.string.contact_already_exists)); + } else { + xmppConnectionService.createContact(contact); + dialog.dismiss(); + switchToConversation(contact); + } + } else { + jid.setError(getString(R.string.invalid_jid)); + } + } + }); + + } + + @SuppressLint("InflateParams") + protected void showJoinConferenceDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.join_conference); + View dialogView = getLayoutInflater().inflate( + R.layout.join_conference_dialog, null); + final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView + .findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, mKnownConferenceHosts)); + populateAccountSpinner(spinner); + final CheckBox bookmarkCheckBox = (CheckBox) dialogView + .findViewById(R.id.bookmark); + builder.setView(dialogView); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.join, null); + final AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener( + new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (!xmppConnectionServiceBound) { + return; + } + if (Validator.isValidJid(jid.getText().toString())) { + String accountJid = (String) spinner + .getSelectedItem(); + String conferenceJid = jid.getText().toString(); + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + if (bookmarkCheckBox.isChecked()) { + if (account.hasBookmarkFor(conferenceJid)) { + jid.setError(getString(R.string.bookmark_already_exists)); + } else { + Bookmark bookmark = new Bookmark(account, + conferenceJid); + bookmark.setAutojoin(true); + account.getBookmarks().add(bookmark); + xmppConnectionService + .pushBookmarks(account); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + conversation.setBookmark(bookmark); + if (!conversation.getMucOptions().online()) { + xmppConnectionService + .joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } else { + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } else { + jid.setError(getString(R.string.invalid_jid)); + } + } + }); + } + + protected void switchToConversation(Contact contact) { + Conversation conversation = xmppConnectionService + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); + switchToConversation(conversation); + } + + private void populateAccountSpinner(Spinner spinner) { + ArrayAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, mActivatedAccounts); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + this.mOptionsMenu = menu; + getMenuInflater().inflate(R.menu.start_conversation, menu); + MenuItem menuCreateContact = (MenuItem) menu + .findItem(R.id.action_create_contact); + MenuItem menuCreateConference = (MenuItem) menu + .findItem(R.id.action_join_conference); + mMenuSearchView = (MenuItem) menu.findItem(R.id.action_search); + mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); + View mSearchView = mMenuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + if (getActionBar().getSelectedNavigationIndex() == 0) { + menuCreateConference.setVisible(false); + } else { + menuCreateContact.setVisible(false); + } + if (mInitialJid != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.append(mInitialJid); + filter(mInitialJid); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_create_contact: + showCreateContactDialog(null); + break; + case R.id.action_join_conference: + showJoinConferenceDialog(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) { + mOptionsMenu.findItem(R.id.action_search).expandActionView(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onBackendConnected() { + xmppConnectionService.setOnRosterUpdateListener(this.onRosterUpdate); + this.mActivatedAccounts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + this.mActivatedAccounts.add(account.getJid()); + } + } + this.mKnownHosts = xmppConnectionService.getKnownHosts(); + this.mKnownConferenceHosts = xmppConnectionService + .getKnownConferenceHosts(); + if (!startByIntent()) { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } else { + filter(null); + } + } + } + + protected boolean startByIntent() { + if (getIntent() != null + && Intent.ACTION_SENDTO.equals(getIntent().getAction())) { + try { + String jid = URLDecoder.decode( + getIntent().getData().getEncodedPath(), "UTF-8").split( + "/")[1]; + setIntent(null); + return handleJid(jid); + } catch (UnsupportedEncodingException e) { + setIntent(null); + return false; + } + } else if (getIntent() != null + && Intent.ACTION_VIEW.equals(getIntent().getAction())) { + Uri uri = getIntent().getData(); + String jid = uri.getSchemeSpecificPart().split("\\?")[0]; + return handleJid(jid); + } + return false; + } + + private boolean handleJid(String jid) { + List contacts = xmppConnectionService.findContacts(jid); + if (contacts.size() == 0) { + showCreateContactDialog(jid); + return false; + } else if (contacts.size() == 1) { + switchToConversation(contacts.get(0)); + return true; + } else { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(jid); + filter(jid); + } else { + mInitialJid = jid; + } + return true; + } + } + + protected void filter(String needle) { + if (xmppConnectionServiceBound) { + this.filterContacts(needle); + this.filterConferences(needle); + } + } + + protected void filterContacts(String needle) { + this.contacts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && contact.match(needle)) { + this.contacts.add(contact); + } + } + } + } + Collections.sort(this.contacts); + mContactsAdapter.notifyDataSetChanged(); + } + + protected void filterConferences(String needle) { + this.conferences.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Bookmark bookmark : account.getBookmarks()) { + if (bookmark.match(needle)) { + this.conferences.add(bookmark); + } + } + } + } + Collections.sort(this.conferences); + mConferenceAdapter.notifyDataSetChanged(); + } + + private void onTabChanged() { + invalidateOptionsMenu(); + } + + public static class MyListFragment extends ListFragment { + private AdapterView.OnItemClickListener mOnItemClickListener; + private int mResContextMenu; + + public void setContextMenu(int res) { + this.mResContextMenu = res; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + if (mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(l, v, position, id); + } + } + + public void setOnListItemClickListener(AdapterView.OnItemClickListener l) { + this.mOnItemClickListener = l; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + registerForContextMenu(getListView()); + getListView().setFastScrollEnabled(true); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + StartConversationActivity activity = (StartConversationActivity) getActivity(); + activity.getMenuInflater().inflate(mResContextMenu, menu); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + if (mResContextMenu == R.menu.conference_context) { + activity.conference_context_id = acmi.position; + } else { + activity.contact_context_id = acmi.position; + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + StartConversationActivity activity = (StartConversationActivity) getActivity(); + switch (item.getItemId()) { + case R.id.context_start_conversation: + activity.openConversationForContact(); + break; + case R.id.context_contact_details: + activity.openDetailsForContact(); + break; + case R.id.context_delete_contact: + activity.deleteContact(); + break; + case R.id.context_join_conference: + activity.openConversationForBookmark(); + break; + case R.id.context_delete_conference: + activity.deleteConference(); + } + return true; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java new file mode 100644 index 000000000..c80199e17 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; + +public interface UiCallback { + public void success(T object); + + public void error(int errorCode, T object); + + public void userInputRequried(PendingIntent pi, T object); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java new file mode 100644 index 000000000..d26f0e31d --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -0,0 +1,637 @@ +package eu.siacs.conversations.ui; + +import java.io.FileNotFoundException; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; +import eu.siacs.conversations.utils.ExceptionHelper; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.AlertDialog.Builder; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.DialogInterface.OnClickListener; +import android.content.IntentSender.SendIntentException; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.text.InputType; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageView; + +public abstract class XmppActivity extends Activity { + + protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; + protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; + + public XmppConnectionService xmppConnectionService; + public boolean xmppConnectionServiceBound = false; + protected boolean handledViewIntent = false; + + protected int mPrimaryTextColor; + protected int mSecondaryTextColor; + protected int mSecondaryBackgroundColor; + protected int mColorRed; + protected int mColorOrange; + protected int mColorGreen; + protected int mPrimaryColor; + + protected boolean mUseSubject = true; + + private DisplayMetrics metrics; + + protected interface OnValueEdited { + public void onValueEdited(String value); + } + + public interface OnPresenceSelected { + public void onPresenceSelected(); + } + + protected ServiceConnection mConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + XmppConnectionBinder binder = (XmppConnectionBinder) service; + xmppConnectionService = binder.getService(); + xmppConnectionServiceBound = true; + onBackendConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + xmppConnectionServiceBound = false; + } + }; + + @Override + protected void onStart() { + super.onStart(); + if (!xmppConnectionServiceBound) { + connectToBackend(); + } + } + + public void connectToBackend() { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction("ui"); + startService(intent); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onStop() { + super.onStop(); + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + } + + protected void hideKeyboard() { + InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + View focus = getCurrentFocus(); + + if (focus != null) { + + inputManager.hideSoftInputFromWindow(focus.getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + public boolean hasPgp() { + return xmppConnectionService.getPgpEngine() != null; + } + + public void showInstallPgpDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.openkeychain_required)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getText(R.string.openkeychain_required_long)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton(getString(R.string.restart), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + stopService(new Intent(XmppActivity.this, + XmppConnectionService.class)); + finish(); + } + }); + builder.setPositiveButton(getString(R.string.install), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri + .parse("market://details?id=org.sufficientlysecure.keychain"); + Intent marketIntent = new Intent(Intent.ACTION_VIEW, + uri); + PackageManager manager = getApplicationContext() + .getPackageManager(); + List infos = manager + .queryIntentActivities(marketIntent, 0); + if (infos.size() > 0) { + startActivity(marketIntent); + } else { + uri = Uri.parse("http://www.openkeychain.org/"); + Intent browserIntent = new Intent( + Intent.ACTION_VIEW, uri); + startActivity(browserIntent); + } + finish(); + } + }); + builder.create().show(); + } + + abstract void onBackendConnected(); + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + break; + case R.id.action_accounts: + startActivity(new Intent(this, ManageAccountActivity.class)); + break; + case android.R.id.home: + finish(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + metrics = getResources().getDisplayMetrics(); + ExceptionHelper.init(getApplicationContext()); + mPrimaryTextColor = getResources().getColor(R.color.primarytext); + mSecondaryTextColor = getResources().getColor(R.color.secondarytext); + mColorRed = getResources().getColor(R.color.red); + mColorOrange = getResources().getColor(R.color.orange); + mColorGreen = getResources().getColor(R.color.green); + mPrimaryColor = getResources().getColor(R.color.primary); + mSecondaryBackgroundColor = getResources().getColor( + R.color.secondarybackground); + if (getPreferences().getBoolean("use_larger_font", false)) { + setTheme(R.style.ConversationsTheme_LargerText); + } + mUseSubject = getPreferences().getBoolean("use_subject", true); + } + + protected SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + } + + public boolean useSubjectToIdentifyConference() { + return mUseSubject; + } + + public void switchToConversation(Conversation conversation) { + switchToConversation(conversation, null, false); + } + + public void switchToConversation(Conversation conversation, String text, + boolean newTask) { + Intent viewConversationIntent = new Intent(this, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversation.getUuid()); + if (text != null) { + viewConversationIntent.putExtra(ConversationActivity.TEXT, text); + } + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + if (newTask) { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } else { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } + startActivity(viewConversationIntent); + finish(); + } + + public void switchToContactDetails(Contact contact) { + Intent intent = new Intent(this, ContactDetailsActivity.class); + intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); + intent.putExtra("account", contact.getAccount().getJid()); + intent.putExtra("contact", contact.getJid()); + startActivity(intent); + } + + public void switchToAccount(Account account) { + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid()); + startActivity(intent); + } + + protected void inviteToConversation(Conversation conversation) { + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + intent.putExtra("conversation", conversation.getUuid()); + startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION); + } + + protected void announcePgp(Account account, final Conversation conversation) { + xmppConnectionService.getPgpEngine().generateSignature(account, + "online", new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Account account) { + try { + startIntentSenderForResult(pi.getIntentSender(), + REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + } catch (SendIntentException e) { + } + } + + @Override + public void success(Account account) { + xmppConnectionService.databaseBackend + .updateAccount(account); + xmppConnectionService.sendPresencePacket(account, + xmppConnectionService.getPresenceGenerator() + .sendPresence(account)); + if (conversation != null) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + xmppConnectionService.databaseBackend + .updateConversation(conversation); + } + } + + @Override + public void error(int error, Account account) { + displayErrorDialog(error); + } + }); + } + + protected void displayErrorDialog(final int errorCode) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder( + XmppActivity.this); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setTitle(getString(R.string.error)); + builder.setMessage(errorCode); + builder.setNeutralButton(R.string.accept, null); + builder.create().show(); + } + }); + + } + + protected void showAddToRosterDialog(final Conversation conversation) { + String jid = conversation.getContactJid(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(jid); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String jid = conversation.getContactJid(); + Account account = conversation.getAccount(); + Contact contact = account.getRoster().getContact(jid); + xmppConnectionService.createContact(contact); + switchToContactDetails(contact); + } + }); + builder.create().show(); + } + + private void showAskForPresenceDialog(final Contact contact) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(contact.getJid()); + builder.setMessage(R.string.request_presence_updates); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.request_now, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + xmppConnectionService.sendPresencePacket(contact + .getAccount(), xmppConnectionService + .getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } + } + }); + builder.create().show(); + } + + private void warnMutalPresenceSubscription(final Conversation conversation, + final OnPresenceSelected listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(conversation.getContact().getJid()); + builder.setMessage(R.string.without_mutual_presence_updates); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ignore, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(null); + if (listener != null) { + listener.onPresenceSelected(); + } + } + }); + builder.create().show(); + } + + protected void quickEdit(String previousValue, OnValueEdited callback) { + quickEdit(previousValue, callback, false); + } + + protected void quickPasswordEdit(String previousValue, + OnValueEdited callback) { + quickEdit(previousValue, callback, true); + } + + @SuppressLint("InflateParams") + private void quickEdit(final String previousValue, + final OnValueEdited callback, boolean password) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = (View) getLayoutInflater() + .inflate(R.layout.quickedit, null); + final EditText editor = (EditText) view.findViewById(R.id.editor); + OnClickListener mClickListener = new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String value = editor.getText().toString(); + if (!previousValue.equals(value) && value.trim().length() > 0) { + callback.onValueEdited(value); + } + } + }; + if (password) { + editor.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_PASSWORD); + editor.setHint(R.string.password); + builder.setPositiveButton(R.string.accept, mClickListener); + } else { + builder.setPositiveButton(R.string.edit, mClickListener); + } + editor.requestFocus(); + editor.setText(previousValue); + builder.setView(view); + builder.setNegativeButton(R.string.cancel, null); + builder.create().show(); + } + + public void selectPresence(final Conversation conversation, + final OnPresenceSelected listener) { + Contact contact = conversation.getContact(); + if (!contact.showInRoster()) { + showAddToRosterDialog(conversation); + } else { + Presences presences = contact.getPresences(); + if (presences.size() == 0) { + if (!contact.getOption(Contact.Options.TO) + && !contact.getOption(Contact.Options.ASKING) + && contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + showAskForPresenceDialog(contact); + } else if (!contact.getOption(Contact.Options.TO) + || !contact.getOption(Contact.Options.FROM)) { + warnMutalPresenceSubscription(conversation, listener); + } else { + conversation.setNextPresence(null); + listener.onPresenceSelected(); + } + } else if (presences.size() == 1) { + String presence = (String) presences.asStringArray()[0]; + conversation.setNextPresence(presence); + listener.onPresenceSelected(); + } else { + final StringBuilder presence = new StringBuilder(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.choose_presence)); + final String[] presencesArray = presences.asStringArray(); + int preselectedPresence = 0; + for (int i = 0; i < presencesArray.length; ++i) { + if (presencesArray[i].equals(contact.lastseen.presence)) { + preselectedPresence = i; + break; + } + } + presence.append(presencesArray[preselectedPresence]); + builder.setSingleChoiceItems(presencesArray, + preselectedPresence, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + presence.delete(0, presence.length()); + presence.append(presencesArray[which]); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(presence.toString()); + 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) { + String contactJid = data.getStringExtra("contact"); + String conversationUuid = data.getStringExtra("conversation"); + Conversation conversation = xmppConnectionService + .findConversationByUuid(conversationUuid); + if (conversation.getMode() == Conversation.MODE_MULTI) { + xmppConnectionService.invite(conversation, contactJid); + } + Log.d(Config.LOGTAG, "inviting " + contactJid + " to " + + conversation.getName()); + } + } + + public int getSecondaryTextColor() { + return this.mSecondaryTextColor; + } + + public int getPrimaryTextColor() { + return this.mPrimaryTextColor; + } + + public int getWarningTextColor() { + return this.mColorRed; + } + + public int getPrimaryColor() { + return this.mPrimaryColor; + } + + public int getSecondaryBackgroundColor() { + return this.mSecondaryBackgroundColor; + } + + public int getPixel(int dp) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + + public AvatarService avatarService() { + return xmppConnectionService.getAvatarService(); + } + + class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Message message = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference(imageView); + } + + @Override + protected Bitmap doInBackground(Message... params) { + message = params[0]; + try { + return xmppConnectionService.getFileBackend().getThumbnail( + message, (int) (metrics.density * 288), false); + } catch (FileNotFoundException e) { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (imageViewReference != null && bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + public void loadBitmap(Message message, ImageView imageView) { + Bitmap bm; + try { + bm = xmppConnectionService.getFileBackend().getThumbnail(message, + (int) (metrics.density * 288), true); + } catch (FileNotFoundException e) { + bm = null; + } + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + if (cancelPotentialWork(message, imageView)) { + imageView.setBackgroundColor(0xff333333); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable( + getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(message); + } catch (RejectedExecutionException e) { + return; + } + } + } + } + + public static boolean cancelPotentialWork(Message message, + ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Message oldMessage = bitmapWorkerTask.message; + if (oldMessage == null || message != oldMessage) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, + BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference( + bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java new file mode 100644 index 000000000..4ca21a3b3 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -0,0 +1,102 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.ui.XmppActivity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class AccountAdapter extends ArrayAdapter { + + private XmppActivity activity; + + public AccountAdapter(XmppActivity activity, List objects) { + super(activity, 0, objects); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + Account account = getItem(position); + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate(R.layout.account_row, parent, false); + } + TextView jid = (TextView) view.findViewById(R.id.account_jid); + jid.setText(account.getJid()); + TextView statusView = (TextView) view.findViewById(R.id.account_status); + ImageView imageView = (ImageView) view.findViewById(R.id.account_image); + imageView.setImageBitmap(activity.avatarService().get(account, + activity.getPixel(48))); + switch (account.getStatus()) { + case Account.STATUS_DISABLED: + statusView.setText(getContext().getString( + R.string.account_status_disabled)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_ONLINE: + statusView.setText(getContext().getString( + R.string.account_status_online)); + statusView.setTextColor(activity.getPrimaryColor()); + break; + case Account.STATUS_CONNECTING: + statusView.setText(getContext().getString( + R.string.account_status_connecting)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_OFFLINE: + statusView.setText(getContext().getString( + R.string.account_status_offline)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_UNAUTHORIZED: + statusView.setText(getContext().getString( + R.string.account_status_unauthorized)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_SERVER_NOT_FOUND: + statusView.setText(getContext().getString( + R.string.account_status_not_found)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_NO_INTERNET: + statusView.setText(getContext().getString( + R.string.account_status_no_internet)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_FAILED: + statusView.setText(getContext().getString( + R.string.account_status_regis_fail)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_CONFLICT: + statusView.setText(getContext().getString( + R.string.account_status_regis_conflict)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_SUCCESSFULL: + statusView.setText(getContext().getString( + R.string.account_status_regis_success)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_REGISTRATION_NOT_SUPPORTED: + statusView.setText(getContext().getString( + R.string.account_status_regis_not_sup)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + default: + statusView.setText(""); + break; + } + + return view; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java new file mode 100644 index 000000000..183c89fad --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -0,0 +1,135 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.utils.UIHelper; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class ConversationAdapter extends ArrayAdapter { + + private XmppActivity activity; + + public ConversationAdapter(XmppActivity activity, + List conversations) { + super(activity, 0, conversations); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + LayoutInflater inflater = (LayoutInflater) activity + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate(R.layout.conversation_list_row, + parent, false); + } + Conversation conversation = getItem(position); + if (this.activity instanceof ConversationActivity) { + ConversationActivity activity = (ConversationActivity) this.activity; + if (!activity.isConversationsOverviewHideable()) { + if (conversation == activity.getSelectedConversation()) { + view.setBackgroundColor(activity + .getSecondaryBackgroundColor()); + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + } + TextView convName = (TextView) view + .findViewById(R.id.conversation_name); + if (conversation.getMode() == Conversation.MODE_SINGLE + || activity.useSubjectToIdentifyConference()) { + convName.setText(conversation.getName()); + } else { + convName.setText(conversation.getContactJid().split("/")[0]); + } + TextView mLastMessage = (TextView) view + .findViewById(R.id.conversation_lastmsg); + TextView mTimestamp = (TextView) view + .findViewById(R.id.conversation_lastupdate); + ImageView imagePreview = (ImageView) view + .findViewById(R.id.conversation_lastimage); + + Message message = conversation.getLatestMessage(); + + if (!conversation.isRead()) { + convName.setTypeface(null, Typeface.BOLD); + } else { + convName.setTypeface(null, Typeface.NORMAL); + } + + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + Downloadable d = message.getDownloadable(); + if (d != null) { + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + if (conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.ITALIC); + } else { + mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC); + } + if (d.getStatus() == Downloadable.STATUS_CHECKING) { + mLastMessage.setText(R.string.checking_image); + } else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) { + mLastMessage.setText(R.string.receiving_image); + } else if (d.getStatus() == Downloadable.STATUS_OFFER) { + mLastMessage.setText(R.string.image_offered_for_download); + } else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + mLastMessage.setText(R.string.image_offered_for_download); + } else if (d.getStatus() == Downloadable.STATUS_DELETED) { + mLastMessage.setText(R.string.image_file_deleted); + } else { + mLastMessage.setText(""); + } + } else { + mLastMessage.setVisibility(View.GONE); + imagePreview.setVisibility(View.VISIBLE); + activity.loadBitmap(message, imagePreview); + } + } else { + if ((message.getEncryption() != Message.ENCRYPTION_PGP) + && (message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { + String body = Config.PARSE_EMOTICONS ? UIHelper + .transformAsciiEmoticons(message.getBody()) : message + .getBody(); + mLastMessage.setText(body); + } else { + mLastMessage.setText(R.string.encrypted_message_received); + } + if (!conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.BOLD); + } else { + mLastMessage.setTypeface(null, Typeface.NORMAL); + } + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + } + mTimestamp.setText(UIHelper.readableTimeDifference(getContext(), + conversation.getLatestMessage().getTimeSent())); + + ImageView profilePicture = (ImageView) view + .findViewById(R.id.conversation_image); + profilePicture.setImageBitmap(activity.avatarService().get( + conversation, activity.getPixel(56))); + + return view; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java new file mode 100644 index 000000000..143dfda12 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import android.content.Context; +import android.widget.ArrayAdapter; +import android.widget.Filter; + +public class KnownHostsAdapter extends ArrayAdapter { + private ArrayList domains; + private Filter domainFilter = new Filter() { + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint != null) { + ArrayList suggestions = new ArrayList(); + final String[] split = constraint.toString().split("@"); + if (split.length == 1) { + for (String domain : domains) { + suggestions.add(split[0].toLowerCase(Locale + .getDefault()) + "@" + domain); + } + } else if (split.length == 2) { + for (String domain : domains) { + if (domain.contentEquals(split[1])) { + suggestions.clear(); + break; + } else if (domain.contains(split[1])) { + suggestions.add(split[0].toLowerCase(Locale + .getDefault()) + "@" + domain); + } + } + } else { + return new FilterResults(); + } + FilterResults filterResults = new FilterResults(); + filterResults.values = suggestions; + filterResults.count = suggestions.size(); + return filterResults; + } else { + return new FilterResults(); + } + } + + @Override + protected void publishResults(CharSequence constraint, + FilterResults results) { + ArrayList filteredList = (ArrayList) results.values; + if (results != null && results.count > 0) { + clear(); + for (Object c : filteredList) { + add((String) c); + } + notifyDataSetChanged(); + } + } + }; + + public KnownHostsAdapter(Context context, int viewResourceId, + List mKnownHosts) { + super(context, viewResourceId, mKnownHosts); + domains = new ArrayList(mKnownHosts.size()); + for (String domain : mKnownHosts) { + domains.add(new String(domain)); + } + } + + @Override + public Filter getFilter() { + return domainFilter; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java new file mode 100644 index 000000000..977aa7b57 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -0,0 +1,44 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.XmppActivity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class ListItemAdapter extends ArrayAdapter { + + protected XmppActivity activity; + + public ListItemAdapter(XmppActivity activity, List objects) { + super(activity, 0, objects); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ListItem item = getItem(position); + if (view == null) { + view = (View) inflater.inflate(R.layout.contact, parent, false); + } + TextView name = (TextView) view.findViewById(R.id.contact_display_name); + TextView jid = (TextView) view.findViewById(R.id.contact_jid); + ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); + + jid.setText(item.getJid()); + name.setText(item.getDisplayName()); + picture.setImageBitmap(activity.avatarService().get(item, + activity.getPixel(48))); + return view; + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java new file mode 100644 index 000000000..a9a55cbf4 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -0,0 +1,560 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Message.ImageParams; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.utils.UIHelper; +import android.content.Intent; +import android.graphics.Typeface; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class MessageAdapter extends ArrayAdapter { + + private static final int SENT = 0; + private static final int RECEIVED = 1; + private static final int STATUS = 2; + private static final int NULL = 3; + + private ConversationActivity activity; + + private DisplayMetrics metrics; + + private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + + public MessageAdapter(ConversationActivity activity, List messages) { + super(activity, 0, messages); + this.activity = activity; + metrics = getContext().getResources().getDisplayMetrics(); + } + + public void setOnContactPictureClicked(OnContactPictureClicked listener) { + this.mOnContactPictureClickedListener = listener; + } + + public void setOnContactPictureLongClicked( + OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } + + @Override + public int getViewTypeCount() { + return 4; + } + + @Override + public int getItemViewType(int position) { + if (getItem(position).wasMergedIntoPrevious()) { + return NULL; + } else if (getItem(position).getType() == Message.TYPE_STATUS) { + return STATUS; + } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) { + return RECEIVED; + } else { + return SENT; + } + } + + private void displayStatus(ViewHolder viewHolder, Message message) { + String filesize = null; + String info = null; + boolean error = false; + if (viewHolder.indicatorReceived != null) { + viewHolder.indicatorReceived.setVisibility(View.GONE); + } + boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getMergedStatus() <= Message.STATUS_RECEIVED; + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + ImageParams params = message.getImageParams(); + if (params.size != 0) { + filesize = params.size / 1024 + " KB"; + } + } + switch (message.getMergedStatus()) { + case Message.STATUS_WAITING: + info = getContext().getString(R.string.waiting); + break; + case Message.STATUS_UNSEND: + info = getContext().getString(R.string.sending); + break; + case Message.STATUS_OFFERED: + info = getContext().getString(R.string.offering); + break; + case Message.STATUS_SEND_RECEIVED: + if (activity.indicateReceived()) { + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + break; + case Message.STATUS_SEND_DISPLAYED: + if (activity.indicateReceived()) { + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + break; + case Message.STATUS_SEND_FAILED: + info = getContext().getString(R.string.send_failed); + error = true; + break; + case Message.STATUS_SEND_REJECTED: + info = getContext().getString(R.string.send_rejected); + error = true; + break; + default: + if (multiReceived) { + Contact contact = message.getContact(); + if (contact != null) { + info = contact.getDisplayName(); + } else { + if (message.getPresence() != null) { + info = message.getPresence(); + } else { + info = message.getCounterpart(); + } + } + } + break; + } + if (error) { + viewHolder.time.setTextColor(activity.getWarningTextColor()); + } else { + viewHolder.time.setTextColor(activity.getSecondaryTextColor()); + } + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + viewHolder.indicator.setVisibility(View.VISIBLE); + } + + String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), + message.getMergedTimeSent()); + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info); + } else if ((filesize == null) && (info != null)) { + viewHolder.time.setText(formatedTime + " \u00B7 " + info); + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(formatedTime + " \u00B7 " + filesize); + } else { + viewHolder.time.setText(formatedTime); + } + } else { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info); + } else if ((filesize == null) && (info != null)) { + if (error) { + viewHolder.time.setText(info + " \u00B7 " + formatedTime); + } else { + viewHolder.time.setText(info); + } + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(filesize + " \u00B7 " + formatedTime); + } else { + viewHolder.time.setText(formatedTime); + } + } + } + + private void displayInfoMessage(ViewHolder viewHolder, int r) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getContext().getString(r)); + viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayDecryptionFailed(ViewHolder viewHolder) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getContext().getString( + R.string.decryption_failed)); + viewHolder.messageBody.setTextColor(activity.getWarningTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayTextMessage(ViewHolder viewHolder, Message message) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + if (message.getBody() != null) { + if (message.getType() != Message.TYPE_PRIVATE) { + String body = Config.PARSE_EMOTICONS ? UIHelper + .transformAsciiEmoticons(message.getMergedBody()) + : message.getMergedBody(); + viewHolder.messageBody.setText(body); + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity + .getString(R.string.private_message); + } else { + String to; + if (message.getPresence() != null) { + to = message.getPresence(); + } else { + to = message.getCounterpart(); + } + privateMarker = activity.getString( + R.string.private_message_to, to); + } + SpannableString span = new SpannableString(privateMarker + " " + + message.getBody()); + span.setSpan( + new ForegroundColorSpan(activity + .getSecondaryTextColor()), 0, privateMarker + .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(span); + } + } else { + viewHolder.messageBody.setText(""); + } + viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(true); + } + + private void displayDownloadableMessage(ViewHolder viewHolder, + final Message message, int resid) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(resid); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + startDonwloadable(message); + } + }); + } + + private void displayImageMessage(ViewHolder viewHolder, + final Message message) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); + ImageParams params = message.getImageParams(); + double target = metrics.density * 288; + int scalledW; + int scalledH; + if (params.width <= params.height) { + scalledW = (int) (params.width / ((double) params.height / target)); + scalledH = (int) target; + } else { + scalledW = (int) target; + scalledH = (int) (params.height / ((double) params.width / target)); + } + viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams( + scalledW, scalledH)); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(activity.xmppConnectionService + .getFileBackend().getJingleFileUri(message), "image/*"); + getContext().startActivity(intent); + } + }); + viewHolder.image.setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, + activity.xmppConnectionService.getFileBackend() + .getJingleFileUri(message)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + shareIntent.setType("image/webp"); + getContext().startActivity( + Intent.createChooser(shareIntent, + getContext().getText(R.string.share_with))); + return true; + } + }); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + final Message item = getItem(position); + int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case NULL: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_null, parent, false); + break; + case SENT: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_sent, parent, false); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getAccount(), + activity.getPixel(48))); + viewHolder.download_button = (Button) view + .findViewById(R.id.download_button); + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + viewHolder.indicatorReceived = (ImageView) view + .findViewById(R.id.indicator_received); + view.setTag(viewHolder); + break; + case RECEIVED: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_received, parent, false); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.download_button = (Button) view + .findViewById(R.id.download_button); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(item.getContact(), + activity.getPixel(48))); + } + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + view.setTag(viewHolder); + break; + case STATUS: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_status, parent, false); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getContact(), + activity.getPixel(32))); + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + String name = item.getConversation() + .getName(); + String read = getContext() + .getString( + R.string.contact_has_read_up_to_this_point, + name); + Toast.makeText(getContext(), read, + Toast.LENGTH_SHORT).show(); + } + }); + + } + break; + default: + viewHolder = null; + break; + } + } else { + viewHolder = (ViewHolder) view.getTag(); + } + + if (type == STATUS) { + return view; + } + if (type == NULL) { + if (position == getCount() - 1) { + view.getLayoutParams().height = 1; + } else { + view.getLayoutParams().height = 0; + + } + view.setLayoutParams(view.getLayoutParams()); + return view; + } + + if (viewHolder.contact_picture != null) { + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(item); + ; + } + + } + }); + viewHolder.contact_picture + .setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(item); + return true; + } else { + return false; + } + } + }); + } + + if (type == RECEIVED) { + if (item.getConversation().getMode() == Conversation.MODE_MULTI) { + Contact contact = item.getContact(); + if (contact != null) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService() + .get(contact, activity.getPixel(48))); + } else { + String name = item.getPresence(); + if (name == null) { + name = item.getCounterpart(); + } + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(name, activity.getPixel(48))); + } + } + } + + if (item.getType() == Message.TYPE_IMAGE + || item.getDownloadable() != null) { + Downloadable d = item.getDownloadable(); + if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) { + displayInfoMessage(viewHolder, R.string.receiving_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_CHECKING) { + displayInfoMessage(viewHolder, R.string.checking_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_DELETED) { + displayInfoMessage(viewHolder, R.string.image_file_deleted); + } else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, item, + R.string.download_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, item, + R.string.check_image_filesize); + } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED) + || (item.getEncryption() == Message.ENCRYPTION_NONE) + || (item.getEncryption() == Message.ENCRYPTION_OTR)) { + displayImageMessage(viewHolder, item); + } else if (item.getEncryption() == Message.ENCRYPTION_PGP) { + displayInfoMessage(viewHolder, R.string.encrypted_message); + } else { + displayDecryptionFailed(viewHolder); + } + } else { + if (item.getEncryption() == Message.ENCRYPTION_PGP) { + if (activity.hasPgp()) { + displayInfoMessage(viewHolder, R.string.encrypted_message); + } else { + displayInfoMessage(viewHolder, + R.string.install_openkeychain); + viewHolder.message_box + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.showInstallPgpDialog(); + } + }); + } + } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayDecryptionFailed(viewHolder); + } else { + displayTextMessage(viewHolder, item); + } + } + + displayStatus(viewHolder, item); + + return view; + } + + public void startDonwloadable(Message message) { + Downloadable downloadable = message.getDownloadable(); + if (downloadable != null) { + if (!downloadable.start()) { + Toast.makeText(activity, R.string.not_connected_try_again, + Toast.LENGTH_SHORT).show(); + } + } + } + + private static class ViewHolder { + + protected LinearLayout message_box; + protected Button download_button; + protected ImageView image; + protected ImageView indicator; + protected ImageView indicatorReceived; + protected TextView time; + protected TextView messageBody; + protected ImageView contact_picture; + + } + + public interface OnContactPictureClicked { + public void onContactPictureClicked(Message message); + } + + public interface OnContactPictureLongClicked { + public void onContactPictureLongClicked(Message message); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java new file mode 100644 index 000000000..47595c6e3 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -0,0 +1,112 @@ +package eu.siacs.conversations.utils; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import eu.siacs.conversations.entities.Account; +import android.util.Base64; + +public class CryptoHelper { + public static final String FILETRANSFER = "?FILETRANSFERv1:"; + final protected static char[] hexArray = "0123456789abcdef".toCharArray(); + final protected static char[] vowels = "aeiou".toCharArray(); + final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz" + .toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] hexToBytes(String hexString) { + int len = hexString.length(); + byte[] array = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character + .digit(hexString.charAt(i + 1), 16)); + } + return array; + } + + public static String saslPlain(String username, String password) { + String sasl = '\u0000' + username + '\u0000' + password; + return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } + + private static byte[] concatenateByteArrays(byte[] a, byte[] b) { + byte[] result = new byte[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } + + public static String saslDigestMd5(Account account, String challenge, + SecureRandom random) { + try { + String[] challengeParts = new String(Base64.decode(challenge, + Base64.DEFAULT)).split(","); + String nonce = ""; + for (int i = 0; i < challengeParts.length; ++i) { + String[] parts = challengeParts[i].split("="); + if (parts[0].equals("nonce")) { + nonce = parts[1].replace("\"", ""); + } else if (parts[0].equals("rspauth")) { + return null; + } + } + String digestUri = "xmpp/" + account.getServer(); + String nonceCount = "00000001"; + String x = account.getUsername() + ":" + account.getServer() + ":" + + account.getPassword(); + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); + String cNonce = new BigInteger(100, random).toString(32); + byte[] a1 = concatenateByteArrays(y, + (":" + nonce + ":" + cNonce).getBytes(Charset + .defaultCharset())); + String a2 = "AUTHENTICATE:" + digestUri; + String ha1 = bytesToHex(md.digest(a1)); + String ha2 = bytesToHex(md.digest(a2.getBytes(Charset + .defaultCharset()))); + String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + + ":auth:" + ha2; + String response = bytesToHex(md.digest(kd.getBytes(Charset + .defaultCharset()))); + String saslString = "username=\"" + account.getUsername() + + "\",realm=\"" + account.getServer() + "\",nonce=\"" + + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount + + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" + + response + ",charset=utf-8"; + return Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + public static String randomMucName(SecureRandom random) { + return randomWord(3, random) + "." + randomWord(7, random); + } + + protected static String randomWord(int lenght, SecureRandom random) { + StringBuilder builder = new StringBuilder(lenght); + for (int i = 0; i < lenght; ++i) { + if (i % 2 == 0) { + builder.append(consonants[random.nextInt(consonants.length)]); + } else { + builder.append(vowels[random.nextInt(vowels.length)]); + } + } + return builder.toString(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java new file mode 100644 index 000000000..c51a75ac6 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -0,0 +1,185 @@ +package eu.siacs.conversations.utils; + +import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; +import de.measite.minidns.Record; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.record.SRV; +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; +import de.measite.minidns.record.Data; +import de.measite.minidns.util.NameUtil; +import eu.siacs.conversations.Config; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Random; +import java.util.TreeMap; + +import android.os.Bundle; +import android.util.Log; + +public class DNSHelper { + protected static Client client = new Client(); + + public static Bundle getSRVRecord(String host) throws IOException { + String dns[] = client.findDNS(); + + if (dns != null) { + for (String dnsserver : dns) { + InetAddress ip = InetAddress.getByName(dnsserver); + Bundle b = queryDNS(host, ip); + if (b.containsKey("name")) { + return b; + } else if (b.containsKey("error") + && "nosrv".equals(b.getString("error", null))) { + return b; + } + } + } + return queryDNS(host, InetAddress.getByName("8.8.8.8")); + } + + public static Bundle queryDNS(String host, InetAddress dnsServer) { + Bundle namePort = new Bundle(); + try { + String qname = "_xmpp-client._tcp." + host; + Log.d(Config.LOGTAG, + "using dns server: " + dnsServer.getHostAddress() + + " to look up " + host); + DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, + dnsServer.getHostAddress()); + + // How should we handle priorities and weight? + // Wikipedia has a nice article about priorities vs. weights: + // https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability + + // we bucket the SRV records based on priority, pick per priority + // a random order respecting the weight, and dump that priority by + // priority + + TreeMap> priorities = new TreeMap>(); + TreeMap> ips4 = new TreeMap>(); + TreeMap> ips6 = new TreeMap>(); + + for (Record[] rrset : new Record[][] { message.getAnswers(), + message.getAdditionalResourceRecords() }) { + for (Record rr : rrset) { + Data d = rr.getPayload(); + if (d instanceof SRV + && NameUtil.idnEquals(qname, rr.getName())) { + SRV srv = (SRV) d; + if (!priorities.containsKey(srv.getPriority())) { + priorities.put(srv.getPriority(), + new ArrayList(2)); + } + priorities.get(srv.getPriority()).add(srv); + } + if (d instanceof A) { + A arecord = (A) d; + if (!ips4.containsKey(rr.getName())) { + ips4.put(rr.getName(), new ArrayList(3)); + } + ips4.get(rr.getName()).add(arecord.toString()); + } + if (d instanceof AAAA) { + AAAA aaaa = (AAAA) d; + if (!ips6.containsKey(rr.getName())) { + ips6.put(rr.getName(), new ArrayList(3)); + } + ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); + } + } + } + + Random rnd = new Random(); + ArrayList result = new ArrayList( + priorities.size() * 2 + 1); + for (ArrayList s : priorities.values()) { + + // trivial case + if (s.size() <= 1) { + result.addAll(s); + continue; + } + + long totalweight = 0l; + for (SRV srv : s) { + totalweight += srv.getWeight(); + } + + while (totalweight > 0l && s.size() > 0) { + long p = (rnd.nextLong() & 0x7fffffffffffffffl) + % totalweight; + int i = 0; + while (p > 0) { + p -= s.get(i++).getPriority(); + } + i--; + // remove is expensive, but we have only a few entries + // anyway + SRV srv = s.remove(i); + totalweight -= srv.getWeight(); + result.add(srv); + } + + Collections.shuffle(s, rnd); + result.addAll(s); + + } + + if (result.size() == 0) { + namePort.putString("error", "nosrv"); + return namePort; + } + // we now have a list of servers to try :-) + + // classic name/port pair + String resultName = result.get(0).getName(); + namePort.putString("name", resultName); + namePort.putInt("port", result.get(0).getPort()); + + if (ips4.containsKey(resultName)) { + // we have an ip! + ArrayList ip = ips4.get(resultName); + Collections.shuffle(ip, rnd); + namePort.putString("ipv4", ip.get(0)); + } + if (ips6.containsKey(resultName)) { + ArrayList ip = ips6.get(resultName); + Collections.shuffle(ip, rnd); + namePort.putString("ipv6", ip.get(0)); + } + + // add all other records + int i = 0; + for (SRV srv : result) { + namePort.putString("name" + i, srv.getName()); + namePort.putInt("port" + i, srv.getPort()); + i++; + } + + } catch (SocketTimeoutException e) { + namePort.putString("error", "timeout"); + } catch (Exception e) { + namePort.putString("error", "unhandled"); + } + return namePort; + } + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java new file mode 100644 index 000000000..88fa18ff2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java @@ -0,0 +1,44 @@ +package eu.siacs.conversations.utils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.Thread.UncaughtExceptionHandler; + +import android.content.Context; + +public class ExceptionHandler implements UncaughtExceptionHandler { + + private UncaughtExceptionHandler defaultHandler; + private Context context; + + public ExceptionHandler(Context context) { + this.context = context; + this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + Writer result = new StringWriter(); + PrintWriter printWriter = new PrintWriter(result); + ex.printStackTrace(printWriter); + String stacktrace = result.toString(); + printWriter.close(); + try { + OutputStream os = context.openFileOutput("stacktrace.txt", + Context.MODE_PRIVATE); + os.write(stacktrace.getBytes()); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + this.defaultHandler.uncaughtException(thread, ex); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java new file mode 100644 index 000000000..b5fc88bdd --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -0,0 +1,117 @@ +package eu.siacs.conversations.utils; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.DialogInterface.OnClickListener; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.preference.PreferenceManager; +import android.text.format.DateUtils; +import android.util.Log; + +public class ExceptionHelper { + public static void init(Context context) { + if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler( + context)); + } + } + + public static void checkForCrash(Context context, + final XmppConnectionService service) { + try { + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(context); + boolean neverSend = preferences.getBoolean("never_send", false); + if (neverSend) { + return; + } + List accounts = service.getAccounts(); + Account account = null; + for (int i = 0; i < accounts.size(); ++i) { + if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) { + account = accounts.get(i); + break; + } + } + if (account == null) { + return; + } + final Account finalAccount = account; + FileInputStream file = context.openFileInput("stacktrace.txt"); + InputStreamReader inputStreamReader = new InputStreamReader(file); + BufferedReader stacktrace = new BufferedReader(inputStreamReader); + final StringBuilder report = new StringBuilder(); + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = null; + try { + packageInfo = pm.getPackageInfo(context.getPackageName(), 0); + report.append("Version: " + packageInfo.versionName + '\n'); + report.append("Last Update: " + + DateUtils.formatDateTime(context, + packageInfo.lastUpdateTime, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_DATE) + '\n'); + } catch (NameNotFoundException e) { + } + String line; + while ((line = stacktrace.readLine()) != null) { + report.append(line); + report.append('\n'); + } + file.close(); + context.deleteFile("stacktrace.txt"); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getString(R.string.crash_report_title)); + builder.setMessage(context.getText(R.string.crash_report_message)); + builder.setPositiveButton(context.getText(R.string.send_now), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + + Log.d(Config.LOGTAG, "using account=" + + finalAccount.getJid() + + " to send in stack trace"); + Conversation conversation = service + .findOrCreateConversation(finalAccount, + "bugs@siacs.eu", false); + Message message = new Message(conversation, report + .toString(), Message.ENCRYPTION_NONE); + service.sendMessage(message); + } + }); + builder.setNegativeButton(context.getText(R.string.send_never), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + preferences.edit().putBoolean("never_send", true) + .commit(); + } + }); + builder.create().show(); + } catch (FileNotFoundException e) { + return; + } catch (IOException e) { + return; + } + + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java new file mode 100644 index 000000000..9a6897689 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.utils; + +import java.util.List; + +import android.os.Bundle; + +public interface OnPhoneContactsLoadedListener { + public void onPhoneContactsLoaded(List phoneContacts); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java new file mode 100644 index 000000000..8fe67234e --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java @@ -0,0 +1,327 @@ +package eu.siacs.conversations.utils; + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException + * if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException + * if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class + .forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException + * if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = Security + .getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class + .equals(secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider() + .getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider() + .getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through all + * requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG ( + * {@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException("Failed to read from " + + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream(new FileInputStream( + URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java new file mode 100644 index 000000000..5becc7e79 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.content.Loader.OnLoadCompleteListener; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Profile; + +public class PhoneHelper { + + public static void loadPhoneContacts(Context context, + final OnPhoneContactsLoadedListener listener) { + final List phoneContacts = new ArrayList(); + + 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 + + "\")"; + + CursorLoader mCursorLoader = new CursorLoader(context, + ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, + null); + mCursorLoader.registerListener(0, new OnLoadCompleteListener() { + + @Override + public void onLoadComplete(Loader arg0, Cursor cursor) { + if (cursor == null) { + return; + } + while (cursor.moveToNext()) { + Bundle contact = new Bundle(); + contact.putInt("phoneid", cursor.getInt(cursor + .getColumnIndex(ContactsContract.Data._ID))); + contact.putString( + "displayname", + cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.DISPLAY_NAME))); + contact.putString("photouri", cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.PHOTO_URI))); + contact.putString("lookup", cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); + + contact.putString( + "jid", + cursor.getString(cursor + .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); + phoneContacts.add(contact); + } + if (listener != null) { + listener.onPhoneContactsLoaded(phoneContacts); + } + } + }); + try { + mCursorLoader.startLoading(); + } catch (RejectedExecutionException e) { + if (listener != null) { + listener.onPhoneContactsLoaded(phoneContacts); + } + } + } + + public static Uri getSefliUri(Context context) { + String[] mProjection = new String[] { Profile._ID, Profile.PHOTO_URI }; + Cursor mProfileCursor = context.getContentResolver().query( + Profile.CONTENT_URI, mProjection, null, null, null); + + if (mProfileCursor == null || mProfileCursor.getCount() == 0) { + return null; + } else { + mProfileCursor.moveToFirst(); + String uri = mProfileCursor.getString(1); + if (uri == null) { + return null; + } else { + return Uri.parse(uri); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java new file mode 100644 index 000000000..5141c83c4 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -0,0 +1,225 @@ +package eu.siacs.conversations.utils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.ManageAccountActivity; +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +public class UIHelper { + private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; + private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE; + + public static String readableTimeDifference(Context context, long time) { + return readableTimeDifference(context, time, false); + } + + public static String readableTimeDifferenceFull(Context context, long time) { + return readableTimeDifference(context, time, true); + } + + private static String readableTimeDifference(Context context, long time, + boolean fullDate) { + if (time == 0) { + return context.getString(R.string.just_now); + } + Date date = new Date(time); + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.just_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.minute_ago); + } else if (difference < 60 * 15) { + 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); + } else { + if (fullDate) { + return DateUtils.formatDateTime(context, date.getTime(), + FULL_DATE_FLAGS); + } else { + return DateUtils.formatDateTime(context, date.getTime(), + SHORT_DATE_FLAGS); + } + } + } + + private static boolean today(Date date) { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(date); + cal2.setTimeInMillis(System.currentTimeMillis()); + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) + && cal1.get(Calendar.DAY_OF_YEAR) == cal2 + .get(Calendar.DAY_OF_YEAR); + } + + public static String lastseen(Context context, long time) { + if (time == 0) { + return context.getString(R.string.never_seen); + } + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.last_seen_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.last_seen_min); + } else if (difference < 60 * 60) { + return context.getString(R.string.last_seen_mins, + Math.round(difference / 60.0)); + } else if (difference < 60 * 60 * 2) { + return context.getString(R.string.last_seen_hour); + } else if (difference < 60 * 60 * 24) { + return context.getString(R.string.last_seen_hours, + Math.round(difference / (60.0 * 60.0))); + } else if (difference < 60 * 60 * 48) { + return context.getString(R.string.last_seen_day); + } else { + return context.getString(R.string.last_seen_days, + Math.round(difference / (60.0 * 60.0 * 24.0))); + } + } + + public static void showErrorNotification(Context context, + List accounts) { + NotificationManager mNotificationManager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + List accountsWproblems = new ArrayList(); + for (Account account : accounts) { + if (account.hasErrorStatus()) { + accountsWproblems.add(account); + } + } + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( + context); + if (accountsWproblems.size() == 0) { + mNotificationManager.cancel(1111); + return; + } else if (accountsWproblems.size() == 1) { + mBuilder.setContentTitle(context + .getString(R.string.problem_connecting_to_account)); + mBuilder.setContentText(accountsWproblems.get(0).getJid()); + } else { + mBuilder.setContentTitle(context + .getString(R.string.problem_connecting_to_accounts)); + mBuilder.setContentText(context.getString(R.string.touch_to_fix)); + } + mBuilder.setOngoing(true); + mBuilder.setLights(0xffffffff, 2000, 4000); + mBuilder.setSmallIcon(R.drawable.ic_notification); + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addParentStack(ConversationActivity.class); + + Intent manageAccountsIntent = new Intent(context, + ManageAccountActivity.class); + stackBuilder.addNextIntent(manageAccountsIntent); + + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); + + mBuilder.setContentIntent(resultPendingIntent); + Notification notification = mBuilder.build(); + mNotificationManager.notify(1111, notification); + } + + @SuppressLint("InflateParams") + public static AlertDialog getVerifyFingerprintDialog( + final ConversationActivity activity, + final Conversation conversation, final View msg) { + final Contact contact = conversation.getContact(); + final Account account = conversation.getAccount(); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("Verify fingerprint"); + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_verify_otr, null); + TextView jid = (TextView) view.findViewById(R.id.verify_otr_jid); + TextView fingerprint = (TextView) view + .findViewById(R.id.verify_otr_fingerprint); + TextView yourprint = (TextView) view + .findViewById(R.id.verify_otr_yourprint); + + jid.setText(contact.getJid()); + fingerprint.setText(conversation.getOtrFingerprint()); + yourprint.setText(account.getOtrFingerprint()); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Verify", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + contact.addOtrFingerprint(conversation.getOtrFingerprint()); + msg.setVisibility(View.GONE); + activity.xmppConnectionService.syncRosterToDisk(account); + } + }); + builder.setView(view); + return builder.create(); + } + + private final static class EmoticonPattern { + Pattern pattern; + String replacement; + + EmoticonPattern(String ascii, int unicode) { + this.pattern = Pattern.compile("(?<=(^|\\s))" + ascii + + "(?=(\\s|$))"); + this.replacement = new String(new int[] { unicode, }, 0, 1); + } + + String replaceAll(String body) { + return pattern.matcher(body).replaceAll(replacement); + } + } + + private static final EmoticonPattern[] patterns = new EmoticonPattern[] { + new EmoticonPattern(":-?D", 0x1f600), + new EmoticonPattern("\\^\\^", 0x1f601), + new EmoticonPattern(":'D", 0x1f602), + new EmoticonPattern("\\]-?D", 0x1f608), + new EmoticonPattern(";-?\\)", 0x1f609), + new EmoticonPattern(":-?\\)", 0x1f60a), + new EmoticonPattern("[B8]-?\\)", 0x1f60e), + new EmoticonPattern(":-?\\|", 0x1f610), + new EmoticonPattern(":-?[/\\\\]", 0x1f615), + new EmoticonPattern(":-?\\*", 0x1f617), + new EmoticonPattern(":-?[Ppb]", 0x1f61b), + new EmoticonPattern(":-?\\(", 0x1f61e), + new EmoticonPattern(":-?[0Oo]", 0x1f62e), + new EmoticonPattern("\\\\o/", 0x1F631), }; + + public static String transformAsciiEmoticons(String body) { + if (body != null) { + for (EmoticonPattern p : patterns) { + body = p.replaceAll(body); + } + body = body.trim(); + } + return body; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java b/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java new file mode 100644 index 000000000..00130fa21 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Validator { + public static final Pattern VALID_JID = Pattern.compile( + "^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE); + + public static boolean isValidJid(String jid) { + Matcher matcher = VALID_JID.matcher(jid); + return matcher.find(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java new file mode 100644 index 000000000..4dee07cf7 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java @@ -0,0 +1,12 @@ +package eu.siacs.conversations.utils; + +public class XmlHelper { + public static String encodeEntities(String content) { + content = content.replace("&", "&"); + content = content.replace("<", "<"); + content = content.replace(">", ">"); + content = content.replace("\"", """); + content = content.replace("'", "'"); + return content; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java new file mode 100644 index 000000000..b777c10c8 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java @@ -0,0 +1,54 @@ +package eu.siacs.conversations.utils.zlib; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * ZLibInputStream is a zlib and input stream compatible version of an + * InflaterInputStream. This class solves the incompatibility between + * {@link InputStream#available()} and {@link InflaterInputStream#available()}. + */ +public class ZLibInputStream extends InflaterInputStream { + + /** + * Construct a ZLibInputStream, reading data from the underlying stream. + * + * @param is + * The {@code InputStream} to read data from. + * @throws IOException + * If an {@code IOException} occurs. + */ + public ZLibInputStream(InputStream is) throws IOException { + super(is, new Inflater(), 512); + } + + /** + * Provide a more InputStream compatible version of available. A return + * value of 1 means that it is likly to read one byte without blocking, 0 + * means that the system is known to block for more input. + * + * @return 0 if no data is available, 1 otherwise + * @throws IOException + */ + @Override + public int available() throws IOException { + /* + * This is one of the funny code blocks. InflaterInputStream.available + * violates the contract of InputStream.available, which breaks kXML2. + * + * I'm not sure who's to blame, oracle/sun for a broken api or the + * google guys for mixing a sun bug with a xml reader that can't handle + * it.... + * + * Anyway, this simple if breaks suns distorted reality, but helps to + * use the api as intended. + */ + if (inf.needsInput()) { + return 0; + } + return super.available(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java new file mode 100644 index 000000000..8b3f5e681 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.utils.zlib; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.NoSuchAlgorithmException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +/** + *

+ * Android 2.2 includes Java7 FLUSH_SYNC option, which will be used by this + * Implementation, preferable via reflection. The @hide was remove in API level + * 19. This class might thus go away in the future. + *

+ *

+ * Please use {@link ZLibOutputStream#SUPPORTED} to check for flush + * compatibility. + *

+ */ +public class ZLibOutputStream extends DeflaterOutputStream { + + /** + * The reflection based flush method. + */ + + private final static Method method; + /** + * SUPPORTED is true if a flush compatible method exists. + */ + public final static boolean SUPPORTED; + + /** + * Static block to initialize {@link #SUPPORTED} and {@link #method}. + */ + static { + Method m = null; + try { + m = Deflater.class.getMethod("deflate", byte[].class, int.class, + int.class, int.class); + } catch (SecurityException e) { + } catch (NoSuchMethodException e) { + } + method = m; + SUPPORTED = (method != null); + } + + /** + * Create a new ZLib compatible output stream wrapping the given low level + * stream. ZLib compatiblity means we will send a zlib header. + * + * @param os + * OutputStream The underlying stream. + * @throws IOException + * In case of a lowlevel transfer problem. + * @throws NoSuchAlgorithmException + * In case of a {@link Deflater} error. + */ + public ZLibOutputStream(OutputStream os) throws IOException, + NoSuchAlgorithmException { + super(os, new Deflater(Deflater.BEST_COMPRESSION)); + } + + /** + * Flush the given stream, preferring Java7 FLUSH_SYNC if available. + * + * @throws IOException + * In case of a lowlevel exception. + */ + @Override + public void flush() throws IOException { + if (!SUPPORTED) { + super.flush(); + return; + } + try { + int count = 0; + do { + count = (Integer) method.invoke(def, buf, 0, buf.length, 3); + if (count > 0) { + out.write(buf, 0, count); + } + } while (count > 0); + } catch (IllegalArgumentException e) { + throw new IOException("Can't flush"); + } catch (IllegalAccessException e) { + throw new IOException("Can't flush"); + } catch (InvocationTargetException e) { + throw new IOException("Can't flush"); + } + super.flush(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/Element.java b/conversations/src/main/java/eu/siacs/conversations/xml/Element.java new file mode 100644 index 000000000..4e11ee2cd --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/Element.java @@ -0,0 +1,148 @@ +package eu.siacs.conversations.xml; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import eu.siacs.conversations.utils.XmlHelper; + +public class Element { + protected String name; + protected Hashtable attributes = new Hashtable(); + protected String content; + protected List children = new ArrayList(); + + public Element(String name) { + this.name = name; + } + + public Element addChild(Element child) { + this.content = null; + children.add(child); + return child; + } + + public Element addChild(String name) { + this.content = null; + Element child = new Element(name); + children.add(child); + return child; + } + + public Element addChild(String name, String xmlns) { + this.content = null; + Element child = new Element(name); + child.setAttribute("xmlns", xmlns); + children.add(child); + return child; + } + + public Element setContent(String content) { + this.content = content; + this.children.clear(); + return this; + } + + public Element findChild(String name) { + for (Element child : this.children) { + if (child.getName().equals(name)) { + return child; + } + } + return null; + } + + public Element findChild(String name, String xmlns) { + for (Element child : this.children) { + if (child.getName().equals(name) + && (child.getAttribute("xmlns").equals(xmlns))) { + return child; + } + } + return null; + } + + public boolean hasChild(String name) { + return findChild(name) != null; + } + + public boolean hasChild(String name, String xmlns) { + return findChild(name, xmlns) != null; + } + + public List getChildren() { + return this.children; + } + + public Element setChildren(List children) { + this.children = children; + return this; + } + + public String getContent() { + return content; + } + + public Element setAttribute(String name, String value) { + if (name != null && value != null) { + this.attributes.put(name, value); + } + return this; + } + + public Element setAttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public String getAttribute(String name) { + if (this.attributes.containsKey(name)) { + return this.attributes.get(name); + } else { + return null; + } + } + + public Hashtable getAttributes() { + return this.attributes; + } + + public String toString() { + StringBuilder elementOutput = new StringBuilder(); + if ((content == null) && (children.size() == 0)) { + Tag emptyTag = Tag.empty(name); + emptyTag.setAtttributes(this.attributes); + elementOutput.append(emptyTag.toString()); + } else { + Tag startTag = Tag.start(name); + startTag.setAtttributes(this.attributes); + elementOutput.append(startTag); + if (content != null) { + elementOutput.append(XmlHelper.encodeEntities(content)); + } else { + for (Element child : children) { + elementOutput.append(child.toString()); + } + } + Tag endTag = Tag.end(name); + elementOutput.append(endTag); + } + return elementOutput.toString(); + } + + public String getName() { + return name; + } + + public void clearChildren() { + this.children.clear(); + } + + public void setAttribute(String name, long value) { + this.setAttribute(name, Long.toString(value)); + } + + public void setAttribute(String name, int value) { + this.setAttribute(name, Integer.toString(value)); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java b/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java new file mode 100644 index 000000000..b9ef979ff --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -0,0 +1,104 @@ +package eu.siacs.conversations.xml; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; + +import eu.siacs.conversations.utils.XmlHelper; + +public class Tag { + public static final int NO = -1; + public static final int START = 0; + public static final int END = 1; + public static final int EMPTY = 2; + + protected int type; + protected String name; + protected Hashtable attributes = new Hashtable(); + + protected Tag(int type, String name) { + this.type = type; + this.name = name; + } + + public static Tag no(String text) { + return new Tag(NO, text); + } + + public static Tag start(String name) { + return new Tag(START, name); + } + + public static Tag end(String name) { + return new Tag(END, name); + } + + public static Tag empty(String name) { + return new Tag(EMPTY, name); + } + + public String getName() { + return name; + } + + public String getAttribute(String attrName) { + return this.attributes.get(attrName); + } + + public Tag setAttribute(String attrName, String attrValue) { + this.attributes.put(attrName, attrValue); + return this; + } + + public Tag setAtttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public boolean isStart(String needle) { + if (needle == null) + return false; + return (this.type == START) && (needle.equals(this.name)); + } + + public boolean isEnd(String needle) { + if (needle == null) + return false; + return (this.type == END) && (needle.equals(this.name)); + } + + public boolean isNo() { + return (this.type == NO); + } + + public String toString() { + StringBuilder tagOutput = new StringBuilder(); + tagOutput.append('<'); + if (type == END) { + tagOutput.append('/'); + } + tagOutput.append(name); + if (type != END) { + Set> attributeSet = attributes.entrySet(); + Iterator> it = attributeSet.iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + tagOutput.append(' '); + tagOutput.append(entry.getKey()); + tagOutput.append("=\""); + tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); + tagOutput.append('"'); + } + } + if (type == EMPTY) { + tagOutput.append('/'); + } + tagOutput.append('>'); + return tagOutput.toString(); + } + + public Hashtable getAttributes() { + return this.attributes; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java new file mode 100644 index 000000000..f11c18464 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -0,0 +1,114 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.concurrent.LinkedBlockingQueue; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class TagWriter { + + private OutputStream plainOutputStream; + private OutputStreamWriter outputStream; + private boolean finshed = false; + private LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); + private Thread asyncStanzaWriter = new Thread() { + private boolean shouldStop = false; + + @Override + public void run() { + while (!shouldStop) { + if ((finshed) && (writeQueue.size() == 0)) { + return; + } + try { + AbstractStanza output = writeQueue.take(); + if (outputStream == null) { + shouldStop = true; + } else { + outputStream.write(output.toString()); + outputStream.flush(); + } + } catch (IOException e) { + shouldStop = true; + } catch (InterruptedException e) { + shouldStop = true; + } + } + } + }; + + public TagWriter() { + } + + public void setOutputStream(OutputStream out) throws IOException { + if (out == null) { + throw new IOException(); + } + this.plainOutputStream = out; + this.outputStream = new OutputStreamWriter(out); + } + + public OutputStream getOutputStream() throws IOException { + if (this.plainOutputStream == null) { + throw new IOException(); + } + return this.plainOutputStream; + } + + public TagWriter beginDocument() throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(""); + outputStream.flush(); + return this; + } + + public TagWriter writeTag(Tag tag) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(tag.toString()); + outputStream.flush(); + return this; + } + + public TagWriter writeElement(Element element) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(element.toString()); + outputStream.flush(); + return this; + } + + public TagWriter writeStanzaAsync(AbstractStanza stanza) { + if (finshed) { + return this; + } else { + if (!asyncStanzaWriter.isAlive()) { + try { + asyncStanzaWriter.start(); + } catch (IllegalThreadStateException e) { + // already started + } + } + writeQueue.add(stanza); + return this; + } + } + + public void finish() { + this.finshed = true; + } + + public boolean finished() { + return (this.writeQueue.size() == 0); + } + + public boolean isActive() { + return outputStream != null; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java new file mode 100644 index 000000000..52d3d46ac --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -0,0 +1,141 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import eu.siacs.conversations.Config; + +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.util.Xml; + +public class XmlReader { + private XmlPullParser parser; + private PowerManager.WakeLock wakeLock; + private InputStream is; + + public XmlReader(WakeLock wakeLock) { + this.parser = Xml.newPullParser(); + try { + this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, + true); + } catch (XmlPullParserException e) { + Log.d(Config.LOGTAG, "error setting namespace feature on parser"); + } + this.wakeLock = wakeLock; + } + + public void setInputStream(InputStream inputStream) throws IOException { + if (inputStream == null) { + throw new IOException(); + } + this.is = inputStream; + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } + + public InputStream getInputStream() throws IOException { + if (this.is == null) { + throw new IOException(); + } + return is; + } + + public void reset() throws IOException { + if (this.is == null) { + throw new IOException(); + } + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } + + public Tag readTag() throws XmlPullParserException, IOException { + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + try { + while (this.is != null + && parser.next() != XmlPullParser.END_DOCUMENT) { + wakeLock.acquire(); + if (parser.getEventType() == XmlPullParser.START_TAG) { + Tag tag = Tag.start(parser.getName()); + for (int i = 0; i < parser.getAttributeCount(); ++i) { + tag.setAttribute(parser.getAttributeName(i), + parser.getAttributeValue(i)); + } + String xmlns = parser.getNamespace(); + if (xmlns != null) { + tag.setAttribute("xmlns", xmlns); + } + return tag; + } else if (parser.getEventType() == XmlPullParser.END_TAG) { + Tag tag = Tag.end(parser.getName()); + return tag; + } else if (parser.getEventType() == XmlPullParser.TEXT) { + Tag tag = Tag.no(parser.getText()); + return tag; + } + } + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new IOException( + "xml parser mishandled ArrayIndexOufOfBounds", e); + } catch (StringIndexOutOfBoundsException e) { + throw new IOException( + "xml parser mishandled StringIndexOufOfBounds", e); + } catch (NullPointerException e) { + throw new IOException("xml parser mishandled NullPointerException", + e); + } catch (IndexOutOfBoundsException e) { + throw new IOException("xml parser mishandled IndexOutOfBound", e); + } + return null; + } + + public Element readElement(Tag currentTag) throws XmlPullParserException, + IOException { + Element element = new Element(currentTag.getName()); + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + if (nextTag.isNo()) { + element.setContent(nextTag.getName()); + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + } + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = this.readElement(nextTag); + element.addChild(child); + } + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + } + return element; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java new file mode 100644 index 000000000..f09cf33dd --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnBindListener { + public void onBind(Account account); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java new file mode 100644 index 000000000..849e8e764 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Contact; + +public interface OnContactStatusChanged { + public void onContactStatusChanged(Contact contact, boolean online); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java new file mode 100644 index 000000000..a4cff9863 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public interface OnIqPacketReceived extends PacketReceived { + public void onIqPacketReceived(Account account, IqPacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java new file mode 100644 index 000000000..5f670d933 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnMessageAcknowledged { + public void onMessageAcknowledged(Account account, String id); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java new file mode 100644 index 000000000..325e945f0 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public interface OnMessagePacketReceived extends PacketReceived { + public void onMessagePacketReceived(Account account, MessagePacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java new file mode 100644 index 000000000..95c1acfcc --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public interface OnPresencePacketReceived extends PacketReceived { + public void onPresencePacketReceived(Account account, PresencePacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java new file mode 100644 index 000000000..ad1d98cb9 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnStatusChanged { + public void onStatusChanged(Account account); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java new file mode 100644 index 000000000..d4502d734 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp; + +public abstract interface PacketReceived { + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java new file mode 100644 index 000000000..903dc59d2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -0,0 +1,1130 @@ +package eu.siacs.conversations.xmpp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.xmlpull.v1.XmlPullParserException; + +import de.duenndns.ssl.MemorizingTrustManager; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.SparseArray; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.DNSHelper; +import eu.siacs.conversations.utils.zlib.ZLibOutputStream; +import eu.siacs.conversations.utils.zlib.ZLibInputStream; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Tag; +import eu.siacs.conversations.xml.TagWriter; +import eu.siacs.conversations.xml.XmlReader; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket; +import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; + +public class XmppConnection implements Runnable { + + protected Account account; + + private WakeLock wakeLock; + + private SecureRandom mRandom; + + private Socket socket; + private XmlReader tagReader; + private TagWriter tagWriter; + + private Features features = new Features(this); + + private boolean shouldBind = true; + private boolean shouldAuthenticate = true; + private Element streamFeatures; + private HashMap> disco = new HashMap>(); + + private String streamId = null; + private int smVersion = 3; + private SparseArray messageReceipts = new SparseArray(); + + private boolean usingCompression = false; + private boolean usingEncryption = false; + + private int stanzasReceived = 0; + private int stanzasSent = 0; + + private long lastPaketReceived = 0; + private long lastPingSent = 0; + private long lastConnect = 0; + private long lastSessionStarted = 0; + + private int attempt = 0; + + private static final int PACKET_IQ = 0; + private static final int PACKET_MESSAGE = 1; + private static final int PACKET_PRESENCE = 2; + + private Hashtable packetCallbacks = new Hashtable(); + private OnPresencePacketReceived presenceListener = null; + private OnJinglePacketReceived jingleListener = null; + private OnIqPacketReceived unregisteredIqListener = null; + private OnMessagePacketReceived messageListener = null; + private OnStatusChanged statusListener = null; + private OnBindListener bindListener = null; + private OnMessageAcknowledged acknowledgedListener = null; + private MemorizingTrustManager mMemorizingTrustManager; + private final Context applicationContext; + + public XmppConnection(Account account, XmppConnectionService service) { + this.mRandom = service.getRNG(); + this.mMemorizingTrustManager = service.getMemorizingTrustManager(); + this.account = account; + this.wakeLock = service.getPowerManager().newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, account.getJid()); + tagWriter = new TagWriter(); + applicationContext = service.getApplicationContext(); + } + + protected void changeStatus(int nextStatus) { + if (account.getStatus() != nextStatus) { + if ((nextStatus == Account.STATUS_OFFLINE) + && (account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_ONLINE) + && (account.getStatus() != Account.STATUS_DISABLED)) { + return; + } + if (nextStatus == Account.STATUS_ONLINE) { + this.attempt = 0; + } + account.setStatus(nextStatus); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + protected void connect() { + Log.d(Config.LOGTAG, account.getJid() + ": connecting"); + usingCompression = false; + usingEncryption = false; + lastConnect = SystemClock.elapsedRealtime(); + lastPingSent = SystemClock.elapsedRealtime(); + this.attempt++; + try { + shouldAuthenticate = shouldBind = !account + .isOptionSet(Account.OPTION_REGISTER); + tagReader = new XmlReader(wakeLock); + tagWriter = new TagWriter(); + packetCallbacks.clear(); + this.changeStatus(Account.STATUS_CONNECTING); + Bundle namePort = DNSHelper.getSRVRecord(account.getServer()); + if ("timeout".equals(namePort.getString("error"))) { + Log.d(Config.LOGTAG, account.getJid() + ": dns timeout"); + this.changeStatus(Account.STATUS_OFFLINE); + return; + } + String srvRecordServer = namePort.getString("name"); + String srvIpServer = namePort.getString("ipv4"); + int srvRecordPort = namePort.getInt("port"); + if (srvRecordServer != null) { + if (srvIpServer != null) { + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + srvRecordServer + + "[" + srvIpServer + "]:" + srvRecordPort); + socket = new Socket(srvIpServer, srvRecordPort); + } else { + boolean socketError = true; + int srvIndex = 0; + while (socketError + && namePort.containsKey("name" + srvIndex)) { + try { + srvRecordServer = namePort.getString("name" + + srvIndex); + srvRecordPort = namePort.getInt("port" + srvIndex); + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + socket = new Socket(srvRecordServer, srvRecordPort); + socketError = false; + } catch (UnknownHostException e) { + srvIndex++; + if (!namePort.containsKey("name" + srvIndex)) { + throw e; + } + } catch (IOException e) { + srvIndex++; + if (!namePort.containsKey("name" + srvIndex)) { + throw e; + } + } + } + } + } else if (namePort.containsKey("error") + && "nosrv".equals(namePort.getString("error", null))) { + socket = new Socket(account.getServer(), 5222); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": timeout in DNS resolution"); + changeStatus(Account.STATUS_OFFLINE); + return; + } + OutputStream out = socket.getOutputStream(); + tagWriter.setOutputStream(out); + InputStream in = socket.getInputStream(); + tagReader.setInputStream(in); + tagWriter.beginDocument(); + sendStartStream(); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("stream")) { + processStream(nextTag); + break; + } else { + Log.d(Config.LOGTAG, + "found unexpected tag: " + nextTag.getName()); + return; + } + } + if (socket.isConnected()) { + socket.close(); + } + } catch (UnknownHostException e) { + this.changeStatus(Account.STATUS_SERVER_NOT_FOUND); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (IOException e) { + this.changeStatus(Account.STATUS_OFFLINE); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (NoSuchAlgorithmException e) { + this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, "compression exception " + e.getMessage()); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (XmlPullParserException e) { + this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, "xml exception " + e.getMessage()); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } + + } + + @Override + public void run() { + connect(); + } + + private void processStream(Tag currentTag) throws XmlPullParserException, + IOException, NoSuchAlgorithmException { + Tag nextTag = tagReader.readTag(); + while ((nextTag != null) && (!nextTag.isEnd("stream"))) { + if (nextTag.isStart("error")) { + processStreamError(nextTag); + } else if (nextTag.isStart("features")) { + processStreamFeatures(nextTag); + } else if (nextTag.isStart("proceed")) { + switchOverToTls(nextTag); + } else if (nextTag.isStart("compressed")) { + switchOverToZLib(nextTag); + } else if (nextTag.isStart("success")) { + Log.d(Config.LOGTAG, account.getJid() + ": logged in"); + tagReader.readTag(); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + tagReader.readElement(nextTag); + changeStatus(Account.STATUS_UNAUTHORIZED); + } else if (nextTag.isStart("challenge")) { + String challange = tagReader.readElement(nextTag).getContent(); + Element response = new Element("response"); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + response.setContent(CryptoHelper.saslDigestMd5(account, + challange, mRandom)); + tagWriter.writeElement(response); + } else if (nextTag.isStart("enabled")) { + Element enabled = tagReader.readElement(nextTag); + if ("true".equals(enabled.getAttribute("resume"))) { + this.streamId = enabled.getAttribute("id"); + Log.d(Config.LOGTAG, account.getJid() + + ": stream managment(" + smVersion + + ") enabled (resumable)"); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": stream managment(" + smVersion + ") enabled"); + } + this.lastSessionStarted = SystemClock.elapsedRealtime(); + this.stanzasReceived = 0; + RequestPacket r = new RequestPacket(smVersion); + tagWriter.writeStanzaAsync(r); + } else if (nextTag.isStart("resumed")) { + lastPaketReceived = SystemClock.elapsedRealtime(); + Element resumed = tagReader.readElement(nextTag); + String h = resumed.getAttribute("h"); + try { + int serverCount = Integer.parseInt(h); + if (serverCount != stanzasSent) { + Log.d(Config.LOGTAG, account.getJid() + + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": session resumed"); + } + if (acknowledgedListener != null) { + for (int i = 0; i < messageReceipts.size(); ++i) { + if (serverCount >= messageReceipts.keyAt(i)) { + acknowledgedListener.onMessageAcknowledged( + account, messageReceipts.valueAt(i)); + } + } + } + messageReceipts.clear(); + } catch (NumberFormatException e) { + + } + sendInitialPing(); + + } else if (nextTag.isStart("r")) { + tagReader.readElement(nextTag); + AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + tagWriter.writeStanzaAsync(ack); + } else if (nextTag.isStart("a")) { + Element ack = tagReader.readElement(nextTag); + lastPaketReceived = SystemClock.elapsedRealtime(); + int serverSequence = Integer.parseInt(ack.getAttribute("h")); + String msgId = this.messageReceipts.get(serverSequence); + if (msgId != null) { + if (this.acknowledgedListener != null) { + this.acknowledgedListener.onMessageAcknowledged( + account, msgId); + } + this.messageReceipts.remove(serverSequence); + } + } else if (nextTag.isStart("failed")) { + tagReader.readElement(nextTag); + Log.d(Config.LOGTAG, account.getJid() + ": resumption failed"); + streamId = null; + if (account.getStatus() != Account.STATUS_ONLINE) { + sendBindRequest(); + } + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isStart("message")) { + processMessage(nextTag); + } else if (nextTag.isStart("presence")) { + processPresence(nextTag); + } + nextTag = tagReader.readTag(); + } + if (account.getStatus() == Account.STATUS_ONLINE) { + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + private void sendInitialPing() { + Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping"); + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setFrom(account.getFullJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, account.getJid() + + ": online with resource " + account.getResource()); + changeStatus(Account.STATUS_ONLINE); + } + }); + } + + private Element processPacket(Tag currentTag, int packetType) + throws XmlPullParserException, IOException { + Element element; + switch (packetType) { + case PACKET_IQ: + element = new IqPacket(); + break; + case PACKET_MESSAGE: + element = new MessagePacket(); + break; + case PACKET_PRESENCE: + element = new PresencePacket(); + break; + default: + return null; + } + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = tagReader.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = tagReader.readElement(nextTag); + String type = currentTag.getAttribute("type"); + if (packetType == PACKET_IQ + && "jingle".equals(child.getName()) + && ("set".equalsIgnoreCase(type) || "get" + .equalsIgnoreCase(type))) { + element = new JinglePacket(); + element.setAttributes(currentTag.getAttributes()); + } + element.addChild(child); + } + nextTag = tagReader.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + } + ++stanzasReceived; + lastPaketReceived = SystemClock.elapsedRealtime(); + return element; + } + + private void processIq(Tag currentTag) throws XmlPullParserException, + IOException { + IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); + + if (packet.getId() == null) { + return; // an iq packet without id is definitely invalid + } + + if (packet instanceof JinglePacket) { + if (this.jingleListener != null) { + this.jingleListener.onJinglePacketReceived(account, + (JinglePacket) packet); + } + } else { + if (packetCallbacks.containsKey(packet.getId())) { + if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) { + ((OnIqPacketReceived) packetCallbacks.get(packet.getId())) + .onIqPacketReceived(account, packet); + } + + packetCallbacks.remove(packet.getId()); + } else if ((packet.getType() == IqPacket.TYPE_GET || packet + .getType() == IqPacket.TYPE_SET) + && this.unregisteredIqListener != null) { + this.unregisteredIqListener.onIqPacketReceived(account, packet); + } + } + } + + private void processMessage(Tag currentTag) throws XmlPullParserException, + IOException { + MessagePacket packet = (MessagePacket) processPacket(currentTag, + PACKET_MESSAGE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) { + ((OnMessagePacketReceived) packetCallbacks.get(id)) + .onMessagePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.messageListener != null) { + this.messageListener.onMessagePacketReceived(account, packet); + } + } + + private void processPresence(Tag currentTag) throws XmlPullParserException, + IOException { + PresencePacket packet = (PresencePacket) processPacket(currentTag, + PACKET_PRESENCE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) { + ((OnPresencePacketReceived) packetCallbacks.get(id)) + .onPresencePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.presenceListener != null) { + this.presenceListener.onPresencePacketReceived(account, packet); + } + } + + private void sendCompressionZlib() throws IOException { + Element compress = new Element("compress"); + compress.setAttribute("xmlns", "http://jabber.org/protocol/compress"); + compress.addChild("method").setContent("zlib"); + tagWriter.writeElement(compress); + } + + private void switchOverToZLib(Tag currentTag) + throws XmlPullParserException, IOException, + NoSuchAlgorithmException { + tagReader.readTag(); // read tag close + tagWriter.setOutputStream(new ZLibOutputStream(tagWriter + .getOutputStream())); + tagReader + .setInputStream(new ZLibInputStream(tagReader.getInputStream())); + + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid() + ": compression enabled"); + usingCompression = true; + processStream(tagReader.readTag()); + } + + private void sendStartTLS() throws IOException { + Tag startTLS = Tag.empty("starttls"); + startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); + tagWriter.writeTag(startTLS); + } + + private SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(applicationContext); + } + + private boolean enableLegacySSL() { + return getPreferences().getBoolean("enable_legacy_ssl", false); + } + + private void switchOverToTls(Tag currentTag) throws XmlPullParserException, + IOException { + tagReader.readTag(); + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, + new X509TrustManager[] { this.mMemorizingTrustManager }, + mRandom); + SSLSocketFactory factory = sc.getSocketFactory(); + + HostnameVerifier verifier = this.mMemorizingTrustManager + .wrapHostnameVerifier(new StrictHostnameVerifier()); + SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, + socket.getInetAddress().getHostAddress(), socket.getPort(), + true); + + // Support all protocols except legacy SSL. + // The min SDK version prevents us having to worry about SSLv2. In + // future, this may be + // true of SSLv3 as well. + final String[] supportProtocols; + if (enableLegacySSL()) { + supportProtocols = sslSocket.getSupportedProtocols(); + } else { + final List supportedProtocols = new LinkedList( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = new String[supportedProtocols.size()]; + supportedProtocols.toArray(supportProtocols); + } + sslSocket.setEnabledProtocols(supportProtocols); + + if (verifier != null + && !verifier.verify(account.getServer(), + sslSocket.getSession())) { + Log.d(Config.LOGTAG, account.getJid() + + ": host mismatch in TLS connection"); + sslSocket.close(); + throw new IOException(); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid() + + ": TLS connection established"); + usingEncryption = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (NoSuchAlgorithmException e1) { + e1.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } + } + + private void sendSaslAuthPlain() throws IOException { + String saslString = CryptoHelper.saslPlain(account.getUsername(), + account.getPassword()); + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "PLAIN"); + auth.setContent(saslString); + tagWriter.writeElement(auth); + } + + private void sendSaslAuthDigestMd5() throws IOException { + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "DIGEST-MD5"); + tagWriter.writeElement(auth); + } + + private void processStreamFeatures(Tag currentTag) + throws XmlPullParserException, IOException { + this.streamFeatures = tagReader.readElement(currentTag); + if (this.streamFeatures.hasChild("starttls") && !usingEncryption) { + sendStartTLS(); + } else if (compressionAvailable()) { + sendCompressionZlib(); + } else if (this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER) + && usingEncryption) { + sendRegistryRequest(); + } else if (!this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER)) { + changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); + disconnect(true); + } else if (this.streamFeatures.hasChild("mechanisms") + && shouldAuthenticate && usingEncryption) { + List mechanisms = extractMechanisms(streamFeatures + .findChild("mechanisms")); + if (mechanisms.contains("PLAIN")) { + sendSaslAuthPlain(); + } else if (mechanisms.contains("DIGEST-MD5")) { + sendSaslAuthDigestMd5(); + } + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + + smVersion) + && streamId != null) { + ResumePacket resume = new ResumePacket(this.streamId, + stanzasReceived, smVersion); + this.tagWriter.writeStanzaAsync(resume); + } else if (this.streamFeatures.hasChild("bind") && shouldBind) { + sendBindRequest(); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": incompatible server. disconnecting"); + disconnect(true); + } + } + + private boolean compressionAvailable() { + if (!this.streamFeatures.hasChild("compression", + "http://jabber.org/features/compress")) + return false; + if (!ZLibOutputStream.SUPPORTED) + return false; + if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) + return false; + + Element compression = this.streamFeatures.findChild("compression", + "http://jabber.org/features/compress"); + for (Element child : compression.getChildren()) { + if (!"method".equals(child.getName())) + continue; + + if ("zlib".equalsIgnoreCase(child.getContent())) { + return true; + } + } + return false; + } + + private List extractMechanisms(Element stream) { + ArrayList mechanisms = new ArrayList(stream + .getChildren().size()); + for (Element child : stream.getChildren()) { + mechanisms.add(child.getContent()); + } + return mechanisms; + } + + private void sendRegistryRequest() { + IqPacket register = new IqPacket(IqPacket.TYPE_GET); + register.query("jabber:iq:register"); + register.setTo(account.getServer()); + sendIqPacket(register, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element instructions = packet.query().findChild("instructions"); + if (packet.query().hasChild("username") + && (packet.query().hasChild("password"))) { + IqPacket register = new IqPacket(IqPacket.TYPE_SET); + Element username = new Element("username") + .setContent(account.getUsername()); + Element password = new Element("password") + .setContent(account.getPassword()); + register.query("jabber:iq:register").addChild(username); + register.query().addChild(password); + sendIqPacket(register, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.setOption(Account.OPTION_REGISTER, + false); + changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL); + } else if (packet.hasChild("error") + && (packet.findChild("error") + .hasChild("conflict"))) { + changeStatus(Account.STATUS_REGISTRATION_CONFLICT); + } else { + changeStatus(Account.STATUS_REGISTRATION_FAILED); + Log.d(Config.LOGTAG, packet.toString()); + } + disconnect(true); + } + }); + } else { + changeStatus(Account.STATUS_REGISTRATION_FAILED); + disconnect(true); + Log.d(Config.LOGTAG, account.getJid() + + ": could not register. instructions are" + + instructions.getContent()); + } + } + }); + } + + private void sendBindRequest() throws IOException { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") + .addChild("resource").setContent(account.getResource()); + this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element bind = packet.findChild("bind"); + if (bind != null) { + Element jid = bind.findChild("jid"); + if (jid != null && jid.getContent() != null) { + account.setResource(jid.getContent().split("/", 2)[1]); + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { + smVersion = 3; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); + } else if (streamFeatures.hasChild("sm", + "urn:xmpp:sm:2")) { + smVersion = 2; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + if (bindListener != null) { + bindListener.onBind(account); + } + sendInitialPing(); + } else { + disconnect(true); + } + } else { + disconnect(true); + } + } + }); + if (this.streamFeatures.hasChild("session")) { + Log.d(Config.LOGTAG, account.getJid() + + ": sending deprecated session"); + IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); + startSession.addChild("session", + "urn:ietf:params:xml:ns:xmpp-session"); + this.sendUnboundIqPacket(startSession, null); + } + } + + private void sendServiceDiscoveryInfo(final String server) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server); + iq.query("http://jabber.org/protocol/disco#info"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + List elements = packet.query().getChildren(); + List features = new ArrayList(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("feature")) { + features.add(elements.get(i).getAttribute("var")); + } + } + disco.put(server, features); + + if (account.getServer().equals(server)) { + enableAdvancedStreamFeatures(); + } + } + }); + } + + private void enableAdvancedStreamFeatures() { + if (getFeatures().carbons()) { + sendEnableCarbons(); + } + } + + private void sendServiceDiscoveryItems(final String server) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server); + iq.query("http://jabber.org/protocol/disco#items"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + List elements = packet.query().getChildren(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("item")) { + String jid = elements.get(i).getAttribute("jid"); + sendServiceDiscoveryInfo(jid); + } + } + } + }); + } + + private void sendEnableCarbons() { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.addChild("enable", "urn:xmpp:carbons:2"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (!packet.hasChild("error")) { + Log.d(Config.LOGTAG, account.getJid() + + ": successfully enabled carbons"); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": error enableing carbons " + packet.toString()); + } + } + }); + } + + private void processStreamError(Tag currentTag) + throws XmlPullParserException, IOException { + Element streamError = tagReader.readElement(currentTag); + if (streamError != null && streamError.hasChild("conflict")) { + String resource = account.getResource().split("\\.")[0]; + account.setResource(resource + "." + nextRandomId()); + Log.d(Config.LOGTAG, + account.getJid() + ": switching resource due to conflict (" + + account.getResource() + ")"); + } + } + + private void sendStartStream() throws IOException { + Tag stream = Tag.start("stream:stream"); + stream.setAttribute("from", account.getJid()); + stream.setAttribute("to", account.getServer()); + stream.setAttribute("version", "1.0"); + stream.setAttribute("xml:lang", "en"); + stream.setAttribute("xmlns", "jabber:client"); + stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); + tagWriter.writeTag(stream); + } + + private String nextRandomId() { + return new BigInteger(50, mRandom).toString(32); + } + + public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + packet.setFrom(account.getFullJid()); + this.sendPacket(packet, callback); + } + + public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + this.sendPacket(packet, callback); + } + + public void sendMessagePacket(MessagePacket packet) { + this.sendPacket(packet, null); + } + + public void sendPresencePacket(PresencePacket packet) { + this.sendPacket(packet, null); + } + + private synchronized void sendPacket(final AbstractStanza packet, + PacketReceived callback) { + if (packet.getName().equals("iq") || packet.getName().equals("message") + || packet.getName().equals("presence")) { + ++stanzasSent; + } + tagWriter.writeStanzaAsync(packet); + if (packet instanceof MessagePacket && packet.getId() != null + && this.streamId != null) { + Log.d(Config.LOGTAG, "request delivery report for stanza " + + stanzasSent); + this.messageReceipts.put(stanzasSent, packet.getId()); + tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); + } + if (callback != null) { + if (packet.getId() == null) { + packet.setId(nextRandomId()); + } + packetCallbacks.put(packet.getId(), callback); + } + } + + public void sendPing() { + if (streamFeatures.hasChild("sm")) { + tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } else { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setFrom(account.getFullJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, null); + } + this.lastPingSent = SystemClock.elapsedRealtime(); + } + + public void setOnMessagePacketReceivedListener( + OnMessagePacketReceived listener) { + this.messageListener = listener; + } + + public void setOnUnregisteredIqPacketReceivedListener( + OnIqPacketReceived listener) { + this.unregisteredIqListener = listener; + } + + public void setOnPresencePacketReceivedListener( + OnPresencePacketReceived listener) { + this.presenceListener = listener; + } + + public void setOnJinglePacketReceivedListener( + OnJinglePacketReceived listener) { + this.jingleListener = listener; + } + + public void setOnStatusChangedListener(OnStatusChanged listener) { + this.statusListener = listener; + } + + public void setOnBindListener(OnBindListener listener) { + this.bindListener = listener; + } + + public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) { + this.acknowledgedListener = listener; + } + + public void disconnect(boolean force) { + Log.d(Config.LOGTAG, account.getJid() + ": disconnecting"); + try { + if (force) { + socket.close(); + return; + } + new Thread(new Runnable() { + + @Override + public void run() { + if (tagWriter.isActive()) { + tagWriter.finish(); + try { + while (!tagWriter.finished()) { + Log.d(Config.LOGTAG, "not yet finished"); + Thread.sleep(100); + } + tagWriter.writeTag(Tag.end("stream:stream")); + socket.close(); + } catch (IOException e) { + Log.d(Config.LOGTAG, + "io exception during disconnect"); + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "interrupted"); + } + } + } + }).start(); + } catch (IOException e) { + Log.d(Config.LOGTAG, "io exception during disconnect"); + } + } + + public List findDiscoItemsByFeature(String feature) { + List items = new ArrayList(); + for (Entry> cursor : disco.entrySet()) { + if (cursor.getValue().contains(feature)) { + items.add(cursor.getKey()); + } + } + return items; + } + + public String findDiscoItemByFeature(String feature) { + List items = findDiscoItemsByFeature(feature); + if (items.size() >= 1) { + return items.get(0); + } + return null; + } + + public void r() { + this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } + + public String getMucServer() { + return findDiscoItemByFeature("http://jabber.org/protocol/muc"); + } + + public int getTimeToNextAttempt() { + int interval = (int) (25 * Math.pow(1.5, attempt)); + int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + return interval - secondsSinceLast; + } + + public int getAttempt() { + return this.attempt; + } + + public Features getFeatures() { + return this.features; + } + + public class Features { + XmppConnection connection; + + public Features(XmppConnection connection) { + this.connection = connection; + } + + private boolean hasDiscoFeature(String server, String feature) { + if (!connection.disco.containsKey(server)) { + return false; + } + return connection.disco.get(server).contains(feature); + } + + public boolean carbons() { + return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); + } + + public boolean sm() { + return streamId != null; + } + + public boolean csi() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("csi", + "urn:xmpp:csi:0"); + } + } + + public boolean pubsub() { + return hasDiscoFeature(account.getServer(), + "http://jabber.org/protocol/pubsub#publish"); + } + + public boolean rosterVersioning() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("ver"); + } + } + + public boolean streamhost() { + return connection + .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; + } + + public boolean compression() { + return connection.usingCompression; + } + } + + public long getLastSessionEstablished() { + long diff; + if (this.lastSessionStarted == 0) { + diff = SystemClock.elapsedRealtime() - this.lastConnect; + } else { + diff = SystemClock.elapsedRealtime() - this.lastSessionStarted; + } + return System.currentTimeMillis() - diff; + } + + public long getLastConnect() { + return this.lastConnect; + } + + public long getLastPingSent() { + return this.lastPingSent; + } + + public long getLastPacketReceived() { + return this.lastPaketReceived; + } + + public void sendActive() { + this.sendPacket(new ActivePacket(), null); + } + + public void sendInactive() { + this.sendPacket(new InactivePacket(), null); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java new file mode 100644 index 000000000..3e7c7b682 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -0,0 +1,143 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.xml.Element; + +public class JingleCandidate { + + public static int TYPE_UNKNOWN; + public static int TYPE_DIRECT = 0; + public static int TYPE_PROXY = 1; + + private boolean ours; + private boolean usedByCounterpart = false; + private String cid; + private String host; + private int port; + private int type; + private String jid; + private int priority; + + public JingleCandidate(String cid, boolean ours) { + this.ours = ours; + this.cid = cid; + } + + public String getCid() { + return cid; + } + + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return this.host; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + + public void setType(int type) { + this.type = type; + } + + public void setType(String type) { + if ("proxy".equals(type)) { + this.type = TYPE_PROXY; + } else if ("direct".equals(type)) { + this.type = TYPE_DIRECT; + } else { + this.type = TYPE_UNKNOWN; + } + } + + public void setPriority(int i) { + this.priority = i; + } + + public int getPriority() { + return this.priority; + } + + public boolean equals(JingleCandidate other) { + return this.getCid().equals(other.getCid()); + } + + public boolean equalValues(JingleCandidate other) { + return other.getHost().equals(this.getHost()) + && (other.getPort() == this.getPort()); + } + + public boolean isOurs() { + return ours; + } + + public int getType() { + return this.type; + } + + public static List parse(List canditates) { + List parsedCandidates = new ArrayList(); + for (Element c : canditates) { + parsedCandidates.add(JingleCandidate.parse(c)); + } + return parsedCandidates; + } + + public static JingleCandidate parse(Element candidate) { + JingleCandidate parsedCandidate = new JingleCandidate( + candidate.getAttribute("cid"), false); + parsedCandidate.setHost(candidate.getAttribute("host")); + parsedCandidate.setJid(candidate.getAttribute("jid")); + parsedCandidate.setType(candidate.getAttribute("type")); + parsedCandidate.setPriority(Integer.parseInt(candidate + .getAttribute("priority"))); + parsedCandidate + .setPort(Integer.parseInt(candidate.getAttribute("port"))); + return parsedCandidate; + } + + public Element toElement() { + Element element = new Element("candidate"); + element.setAttribute("cid", this.getCid()); + element.setAttribute("host", this.getHost()); + element.setAttribute("port", Integer.toString(this.getPort())); + element.setAttribute("jid", this.getJid()); + element.setAttribute("priority", Integer.toString(this.getPriority())); + if (this.getType() == TYPE_DIRECT) { + element.setAttribute("type", "direct"); + } else if (this.getType() == TYPE_PROXY) { + element.setAttribute("type", "proxy"); + } + return element; + } + + public void flagAsUsedByCounterpart() { + this.usedByCounterpart = true; + } + + public boolean isUsedByCounterpart() { + return this.usedByCounterpart; + } + + public String toString() { + return this.getHost() + ":" + this.getPort() + " (prio=" + + this.getPriority() + ")"; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java new file mode 100644 index 000000000..a0b2feb21 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -0,0 +1,910 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnection implements Downloadable { + + private final String[] extensions = { "webp", "jpeg", "jpg", "png" }; + private final String[] cryptoExtensions = { "pgp", "gpg", "otr" }; + + private JingleConnectionManager mJingleConnectionManager; + private XmppConnectionService mXmppConnectionService; + + protected static final int JINGLE_STATUS_INITIATED = 0; + protected static final int JINGLE_STATUS_ACCEPTED = 1; + protected static final int JINGLE_STATUS_TERMINATED = 2; + protected static final int JINGLE_STATUS_CANCELED = 3; + protected static final int JINGLE_STATUS_FINISHED = 4; + protected static final int JINGLE_STATUS_TRANSMITTING = 5; + protected static final int JINGLE_STATUS_FAILED = 99; + + private int ibbBlockSize = 4096; + + private int mJingleStatus = -1; + private int mStatus = -1; + private Message message; + private String sessionId; + private Account account; + private String initiator; + private String responder; + private List candidates = new ArrayList(); + private ConcurrentHashMap connections = new ConcurrentHashMap(); + + private String transportId; + private Element fileOffer; + private DownloadableFile file = null; + + private String contentName; + private String contentCreator; + + private boolean receivedCandidate = false; + private boolean sentCandidate = false; + + private boolean acceptedAutomatically = false; + + private JingleTransport transport = null; + + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + if (initiator.equals(account.getFullJid())) { + mXmppConnectionService.markMessage(message, + Message.STATUS_SEND_FAILED); + } + mJingleStatus = JINGLE_STATUS_FAILED; + } + } + }; + + final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { + + @Override + public void onFileTransmitted(DownloadableFile file) { + if (responder.equals(account.getFullJid())) { + sendSuccess(); + if (acceptedAutomatically) { + message.markUnread(); + JingleConnection.this.mXmppConnectionService + .getNotificationService().push(message); + } + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(Long.toString(file.getSize()) + ',' + + imageWidth + ',' + imageHeight); + mXmppConnectionService.databaseBackend.createMessage(message); + mXmppConnectionService.markMessage(message, + Message.STATUS_RECEIVED); + } + Log.d(Config.LOGTAG, + "sucessfully transmitted file:" + file.getAbsolutePath()); + if (message.getEncryption() != Message.ENCRYPTION_PGP) { + Intent intent = new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + } + } + + @Override + public void onFileTransferAborted() { + JingleConnection.this.sendCancel(); + JingleConnection.this.cancel(); + } + }; + + private OnProxyActivated onProxyActivated = new OnProxyActivated() { + + @Override + public void success() { + if (initiator.equals(account.getFullJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + transport.send(file, onFileTransmissionSatusChanged); + } else { + transport.receive(file, onFileTransmissionSatusChanged); + Log.d(Config.LOGTAG, "we were responding. receiving file"); + } + } + + @Override + public void failed() { + Log.d(Config.LOGTAG, "proxy activation failed"); + } + }; + + public JingleConnection(JingleConnectionManager mJingleConnectionManager) { + this.mJingleConnectionManager = mJingleConnectionManager; + this.mXmppConnectionService = mJingleConnectionManager + .getXmppConnectionService(); + } + + public String getSessionId() { + return this.sessionId; + } + + public Account getAccount() { + return this.account; + } + + public String getCounterPart() { + return this.message.getCounterpart(); + } + + public void deliverPacket(JinglePacket packet) { + boolean returnResult = true; + if (packet.isAction("session-terminate")) { + Reason reason = packet.getReason(); + if (reason != null) { + if (reason.hasChild("cancel")) { + this.cancel(); + } else if (reason.hasChild("success")) { + this.receiveSuccess(); + } else { + this.cancel(); + } + } else { + this.cancel(); + } + } else if (packet.isAction("session-accept")) { + returnResult = receiveAccept(packet); + } else if (packet.isAction("transport-info")) { + returnResult = receiveTransportInfo(packet); + } else if (packet.isAction("transport-replace")) { + if (packet.getJingleContent().hasIbbTransport()) { + returnResult = this.receiveFallbackToIbb(packet); + } else { + returnResult = false; + Log.d(Config.LOGTAG, "trying to fallback to something unknown" + + packet.toString()); + } + } else if (packet.isAction("transport-accept")) { + returnResult = this.receiveTransportAccept(packet); + } else { + Log.d(Config.LOGTAG, "packet arrived in connection. action was " + + packet.getAction()); + returnResult = false; + } + IqPacket response; + if (returnResult) { + response = packet.generateRespone(IqPacket.TYPE_RESULT); + + } else { + response = packet.generateRespone(IqPacket.TYPE_ERROR); + } + account.getXmppConnection().sendIqPacket(response, null); + } + + public void init(Message message) { + this.contentCreator = "initiator"; + this.contentName = this.mJingleConnectionManager.nextRandomId(); + this.message = message; + this.account = message.getConversation().getAccount(); + this.initiator = this.account.getFullJid(); + this.responder = this.message.getCounterpart(); + this.sessionId = this.mJingleConnectionManager.nextRandomId(); + if (this.candidates.size() > 0) { + this.sendInitRequest(); + } else { + this.mJingleConnectionManager.getPrimaryCandidate(account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + if (success) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), + socksConnection); + socksConnection + .connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection to our own primary candidete failed"); + sendInitRequest(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "succesfully connected to our own primary candidate"); + mergeCandidate(candidate); + sendInitRequest(); + } + }); + mergeCandidate(candidate); + } else { + Log.d(Config.LOGTAG, + "no primary candidate of our own was found"); + sendInitRequest(); + } + } + }); + } + + } + + public void init(Account account, JinglePacket packet) { + this.mJingleStatus = JINGLE_STATUS_INITIATED; + Conversation conversation = this.mXmppConnectionService + .findOrCreateConversation(account, + packet.getFrom().split("/", 2)[0], false); + this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); + this.message.setStatus(Message.STATUS_RECEIVED); + this.message.setType(Message.TYPE_IMAGE); + this.mStatus = Downloadable.STATUS_OFFER; + this.message.setDownloadable(this); + String[] fromParts = packet.getFrom().split("/", 2); + this.message.setPresence(fromParts[1]); + this.account = account; + this.initiator = packet.getFrom(); + this.responder = this.account.getFullJid(); + this.sessionId = packet.getSessionId(); + Content content = packet.getJingleContent(); + this.contentCreator = content.getAttribute("creator"); + this.contentName = content.getAttribute("name"); + this.transportId = content.getTransportId(); + this.mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); + this.fileOffer = packet.getJingleContent().getFileOffer(); + if (fileOffer != null) { + Element fileSize = fileOffer.findChild("size"); + Element fileNameElement = fileOffer.findChild("name"); + if (fileNameElement != null) { + boolean supportedFile = false; + String[] filename = fileNameElement.getContent() + .toLowerCase(Locale.US).split("\\."); + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 1])) { + supportedFile = true; + } else if (Arrays.asList(this.cryptoExtensions).contains( + filename[filename.length - 1])) { + if (filename.length == 3) { + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 2])) { + supportedFile = true; + if (filename[filename.length - 1].equals("otr")) { + Log.d(Config.LOGTAG, "receiving otr file"); + this.message + .setEncryption(Message.ENCRYPTION_OTR); + } else { + this.message + .setEncryption(Message.ENCRYPTION_PGP); + } + } + } + } + if (supportedFile) { + long size = Long.parseLong(fileSize.getContent()); + message.setBody(Long.toString(size)); + conversation.add(message); + mXmppConnectionService.updateConversationUi(); + if (size <= this.mJingleConnectionManager + .getAutoAcceptFileSize()) { + Log.d(Config.LOGTAG, "auto accepting file from " + + packet.getFrom()); + this.acceptedAutomatically = true; + this.sendAccept(); + } else { + message.markUnread(); + Log.d(Config.LOGTAG, + "not auto accepting new file offer with size: " + + size + + " allowed size:" + + this.mJingleConnectionManager + .getAutoAcceptFileSize()); + this.mXmppConnectionService.getNotificationService() + .push(message); + } + this.file = this.mXmppConnectionService.getFileBackend() + .getFile(message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + byte[] key = conversation.getSymmetricKey(); + if (key == null) { + this.sendCancel(); + this.cancel(); + return; + } else { + this.file.setKey(key); + } + } + this.file.setExpectedSize(size); + } else { + this.sendCancel(); + this.cancel(); + } + } else { + this.sendCancel(); + this.cancel(); + } + } else { + this.sendCancel(); + this.cancel(); + } + } + + private void sendInitRequest() { + JinglePacket packet = this.bootstrapPacket("session-initiate"); + Content content = new Content(this.contentCreator, this.contentName); + if (message.getType() == Message.TYPE_IMAGE) { + content.setTransportId(this.transportId); + this.file = this.mXmppConnectionService.getFileBackend().getFile( + message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = this.message.getConversation(); + this.mXmppConnectionService.renewSymmetricKey(conversation); + content.setFileOffer(this.file, true); + this.file.setKey(conversation.getSymmetricKey()); + } else { + content.setFileOffer(this.file, false); + } + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setTransportId(this.transportId); + content.socks5transport().setChildren(getCandidatesAsElements()); + packet.setContent(content); + this.sendJinglePacket(packet); + this.mJingleStatus = JINGLE_STATUS_INITIATED; + } + } + + private List getCandidatesAsElements() { + List elements = new ArrayList(); + for (JingleCandidate c : this.candidates) { + elements.add(c.toElement()); + } + return elements; + } + + private void sendAccept() { + mJingleStatus = JINGLE_STATUS_ACCEPTED; + this.mStatus = Downloadable.STATUS_DOWNLOADING; + mXmppConnectionService.updateConversationUi(); + this.mJingleConnectionManager.getPrimaryCandidate(this.account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + final JinglePacket packet = bootstrapPacket("session-accept"); + final Content content = new Content(contentCreator, + contentName); + content.setFileOffer(fileOffer); + content.setTransportId(transportId); + if ((success) && (!equalCandidateExists(candidate))) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection to our own primary candidate failed"); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "connected to primary candidate"); + mergeCandidate(candidate); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + }); + } else { + Log.d(Config.LOGTAG, + "did not find a primary candidate for ourself"); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + } + }); + + } + + private JinglePacket bootstrapPacket(String action) { + JinglePacket packet = new JinglePacket(); + packet.setAction(action); + packet.setFrom(account.getFullJid()); + packet.setTo(this.message.getCounterpart()); + packet.setSessionId(this.sessionId); + packet.setInitiator(this.initiator); + return packet; + } + + private void sendJinglePacket(JinglePacket packet) { + // Log.d(Config.LOGTAG,packet.toString()); + account.getXmppConnection().sendIqPacket(packet, responseListener); + } + + private boolean receiveAccept(JinglePacket packet) { + Content content = packet.getJingleContent(); + mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); + this.mJingleStatus = JINGLE_STATUS_ACCEPTED; + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + this.connectNextCandidate(); + return true; + } + + private boolean receiveTransportInfo(JinglePacket packet) { + Content content = packet.getJingleContent(); + if (content.hasSocks5Transport()) { + if (content.socks5transport().hasChild("activated")) { + if ((this.transport != null) + && (this.transport instanceof JingleSocks5Transport)) { + onProxyActivated.success(); + } else { + String cid = content.socks5transport() + .findChild("activated").getAttribute("cid"); + Log.d(Config.LOGTAG, "received proxy activated (" + cid + + ")prior to choosing our own transport"); + JingleSocks5Transport connection = this.connections + .get(cid); + if (connection != null) { + connection.setActivated(true); + } else { + Log.d(Config.LOGTAG, "activated connection not found"); + this.sendCancel(); + this.cancel(); + } + } + return true; + } else if (content.socks5transport().hasChild("proxy-error")) { + onProxyActivated.failed(); + return true; + } else if (content.socks5transport().hasChild("candidate-error")) { + Log.d(Config.LOGTAG, "received candidate error"); + this.receivedCandidate = true; + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { + this.connect(); + } + return true; + } else if (content.socks5transport().hasChild("candidate-used")) { + String cid = content.socks5transport() + .findChild("candidate-used").getAttribute("cid"); + if (cid != null) { + Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid); + JingleCandidate candidate = getCandidate(cid); + candidate.flagAsUsedByCounterpart(); + this.receivedCandidate = true; + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { + this.connect(); + } else { + Log.d(Config.LOGTAG, + "ignoring because file is already in transmission or we havent sent our candidate yet"); + } + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return true; + } + } + + private void connect() { + final JingleSocks5Transport connection = chooseConnection(); + this.transport = connection; + if (connection == null) { + Log.d(Config.LOGTAG, "could not find suitable candidate"); + this.disconnect(); + if (this.initiator.equals(account.getFullJid())) { + this.sendFallbackToIbb(); + } + } else { + this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; + if (connection.needsActivation()) { + if (connection.getCandidate().isOurs()) { + Log.d(Config.LOGTAG, "candidate " + + connection.getCandidate().getCid() + + " was our proxy. going to activate"); + IqPacket activation = new IqPacket(IqPacket.TYPE_SET); + activation.setTo(connection.getCandidate().getJid()); + activation.query("http://jabber.org/protocol/bytestreams") + .setAttribute("sid", this.getSessionId()); + activation.query().addChild("activate") + .setContent(this.getCounterPart()); + this.account.getXmppConnection().sendIqPacket(activation, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + onProxyActivated.failed(); + } else { + onProxyActivated.success(); + sendProxyActivated(connection + .getCandidate().getCid()); + } + } + }); + } else { + Log.d(Config.LOGTAG, + "candidate " + + connection.getCandidate().getCid() + + " was a proxy. waiting for other party to activate"); + } + } else { + if (initiator.equals(account.getFullJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + connection.send(file, onFileTransmissionSatusChanged); + } else { + Log.d(Config.LOGTAG, "we were responding. receiving file"); + connection.receive(file, onFileTransmissionSatusChanged); + } + } + } + } + + private JingleSocks5Transport chooseConnection() { + JingleSocks5Transport connection = null; + for (Entry cursor : connections + .entrySet()) { + JingleSocks5Transport currentConnection = cursor.getValue(); + // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString()); + if (currentConnection.isEstablished() + && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection + .getCandidate().isOurs()))) { + // Log.d(Config.LOGTAG,"is usable"); + if (connection == null) { + connection = currentConnection; + } else { + if (connection.getCandidate().getPriority() < currentConnection + .getCandidate().getPriority()) { + connection = currentConnection; + } else if (connection.getCandidate().getPriority() == currentConnection + .getCandidate().getPriority()) { + // Log.d(Config.LOGTAG,"found two candidates with same priority"); + if (initiator.equals(account.getFullJid())) { + if (currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } else { + if (!currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } + } + } + } + } + return connection; + } + + private void sendSuccess() { + JinglePacket packet = bootstrapPacket("session-terminate"); + Reason reason = new Reason(); + reason.addChild("success"); + packet.setReason(reason); + this.sendJinglePacket(packet); + this.disconnect(); + this.mJingleStatus = JINGLE_STATUS_FINISHED; + this.message.setStatus(Message.STATUS_RECEIVED); + this.message.setDownloadable(null); + this.mXmppConnectionService.updateMessage(message); + this.mJingleConnectionManager.finishConnection(this); + } + + private void sendFallbackToIbb() { + Log.d(Config.LOGTAG, "sending fallback to ibb"); + JinglePacket packet = this.bootstrapPacket("transport-replace"); + Content content = new Content(this.contentCreator, this.contentName); + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setTransportId(this.transportId); + content.ibbTransport().setAttribute("block-size", + Integer.toString(this.ibbBlockSize)); + packet.setContent(content); + this.sendJinglePacket(packet); + } + + private boolean receiveFallbackToIbb(JinglePacket packet) { + Log.d(Config.LOGTAG, "receiving fallack to ibb"); + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { + int bs = Integer.parseInt(receivedBlockSize); + if (bs > this.ibbBlockSize) { + this.ibbBlockSize = bs; + } + } + this.transportId = packet.getJingleContent().getTransportId(); + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); + this.transport.receive(file, onFileTransmissionSatusChanged); + JinglePacket answer = bootstrapPacket("transport-accept"); + Content content = new Content("initiator", "a-file-offer"); + content.setTransportId(this.transportId); + content.ibbTransport().setAttribute("block-size", + Integer.toString(this.ibbBlockSize)); + answer.setContent(content); + this.sendJinglePacket(answer); + return true; + } + + private boolean receiveTransportAccept(JinglePacket packet) { + if (packet.getJingleContent().hasIbbTransport()) { + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { + int bs = Integer.parseInt(receivedBlockSize); + if (bs > this.ibbBlockSize) { + this.ibbBlockSize = bs; + } + } + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); + this.transport.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, "ibb open failed"); + } + + @Override + public void established() { + JingleConnection.this.transport.send(file, + onFileTransmissionSatusChanged); + } + }); + return true; + } else { + return false; + } + } + + private void receiveSuccess() { + this.mJingleStatus = JINGLE_STATUS_FINISHED; + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND); + this.disconnect(); + this.mJingleConnectionManager.finishConnection(this); + } + + public void cancel() { + this.mJingleStatus = JINGLE_STATUS_CANCELED; + this.disconnect(); + if (this.message != null) { + if (this.responder.equals(account.getFullJid())) { + this.mStatus = Downloadable.STATUS_FAILED; + this.mXmppConnectionService.updateConversationUi(); + } else { + if (this.mJingleStatus == JINGLE_STATUS_INITIATED) { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_REJECTED); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + } + } + } + this.mJingleConnectionManager.finishConnection(this); + } + + private void sendCancel() { + JinglePacket packet = bootstrapPacket("session-terminate"); + Reason reason = new Reason(); + reason.addChild("cancel"); + packet.setReason(reason); + this.sendJinglePacket(packet); + } + + private void connectNextCandidate() { + for (JingleCandidate candidate : this.candidates) { + if ((!connections.containsKey(candidate.getCid()) && (!candidate + .isOurs()))) { + this.connectWithCandidate(candidate); + return; + } + } + this.sendCandidateError(); + } + + private void connectWithCandidate(final JingleCandidate candidate) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + this, candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection failed with " + candidate.getHost() + ":" + + candidate.getPort()); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "established connection with " + candidate.getHost() + + ":" + candidate.getPort()); + sendCandidateUsed(candidate.getCid()); + } + }); + } + + private void disconnect() { + Iterator> it = this.connections + .entrySet().iterator(); + while (it.hasNext()) { + Entry pairs = it.next(); + pairs.getValue().disconnect(); + it.remove(); + } + } + + private void sendProxyActivated(String cid) { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("activated") + .setAttribute("cid", cid); + packet.setContent(content); + this.sendJinglePacket(packet); + } + + private void sendCandidateUsed(final String cid) { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("candidate-used") + .setAttribute("cid", cid); + packet.setContent(content); + this.sentCandidate = true; + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { + connect(); + } + this.sendJinglePacket(packet); + } + + private void sendCandidateError() { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("candidate-error"); + packet.setContent(content); + this.sentCandidate = true; + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { + connect(); + } + this.sendJinglePacket(packet); + } + + public String getInitiator() { + return this.initiator; + } + + public String getResponder() { + return this.responder; + } + + public int getJingleStatus() { + return this.mJingleStatus; + } + + private boolean equalCandidateExists(JingleCandidate candidate) { + for (JingleCandidate c : this.candidates) { + if (c.equalValues(candidate)) { + return true; + } + } + return false; + } + + private void mergeCandidate(JingleCandidate candidate) { + for (JingleCandidate c : this.candidates) { + if (c.equals(candidate)) { + return; + } + } + this.candidates.add(candidate); + } + + private void mergeCandidates(List candidates) { + for (JingleCandidate c : candidates) { + mergeCandidate(c); + } + } + + private JingleCandidate getCandidate(String cid) { + for (JingleCandidate c : this.candidates) { + if (c.getCid().equals(cid)) { + return c; + } + } + return null; + } + + interface OnProxyActivated { + public void success(); + + public void failed(); + } + + public boolean hasTransportId(String sid) { + return sid.equals(this.transportId); + } + + public JingleTransport getTransport() { + return this.transport; + } + + public boolean start() { + if (account.getStatus() == Account.STATUS_ONLINE) { + if (mJingleStatus == JINGLE_STATUS_INITIATED) { + new Thread(new Runnable() { + + @Override + public void run() { + sendAccept(); + } + }).start(); + } + return true; + } else { + return false; + } + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java new file mode 100644 index 000000000..1e7c84d45 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -0,0 +1,163 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import android.annotation.SuppressLint; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnectionManager extends AbstractConnectionManager { + private List connections = new CopyOnWriteArrayList(); + + private HashMap primaryCandidates = new HashMap(); + + @SuppressLint("TrulyRandom") + private SecureRandom random = new SecureRandom(); + + public JingleConnectionManager(XmppConnectionService service) { + super(service); + } + + public void deliverPacket(Account account, JinglePacket packet) { + if (packet.isAction("session-initiate")) { + JingleConnection connection = new JingleConnection(this); + connection.init(account, packet); + connections.add(connection); + } else { + for (JingleConnection connection : connections) { + if (connection.getAccount() == account + && connection.getSessionId().equals( + packet.getSessionId()) + && connection.getCounterPart().equals(packet.getFrom())) { + connection.deliverPacket(packet); + return; + } + } + account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_ERROR), null); + } + } + + public JingleConnection createNewConnection(Message message) { + JingleConnection connection = new JingleConnection(this); + connection.init(message); + this.connections.add(connection); + return connection; + } + + public JingleConnection createNewConnection(JinglePacket packet) { + JingleConnection connection = new JingleConnection(this); + this.connections.add(connection); + return connection; + } + + public void finishConnection(JingleConnection connection) { + this.connections.remove(connection); + } + + public void getPrimaryCandidate(Account account, + final OnPrimaryCandidateFound listener) { + if (!this.primaryCandidates.containsKey(account.getJid())) { + String xmlns = "http://jabber.org/protocol/bytestreams"; + final String proxy = account.getXmppConnection() + .findDiscoItemByFeature(xmlns); + if (proxy != null) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(proxy); + iq.query(xmlns); + account.getXmppConnection().sendIqPacket(iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + Element streamhost = packet + .query() + .findChild("streamhost", + "http://jabber.org/protocol/bytestreams"); + if (streamhost != null) { + JingleCandidate candidate = new JingleCandidate( + nextRandomId(), true); + candidate.setHost(streamhost + .getAttribute("host")); + candidate.setPort(Integer + .parseInt(streamhost + .getAttribute("port"))); + candidate + .setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(proxy); + candidate.setPriority(655360 + 65535); + primaryCandidates.put(account.getJid(), + candidate); + listener.onPrimaryCandidateFound(true, + candidate); + } else { + listener.onPrimaryCandidateFound(false, + null); + } + } + }); + } else { + listener.onPrimaryCandidateFound(false, null); + } + + } else { + listener.onPrimaryCandidateFound(true, + this.primaryCandidates.get(account.getJid())); + } + } + + public String nextRandomId() { + return new BigInteger(50, random).toString(32); + } + + public void deliverIbbPacket(Account account, IqPacket packet) { + String sid = null; + Element payload = null; + if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) { + payload = packet + .findChild("open", "http://jabber.org/protocol/ibb"); + sid = payload.getAttribute("sid"); + } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) { + payload = packet + .findChild("data", "http://jabber.org/protocol/ibb"); + sid = payload.getAttribute("sid"); + } + if (sid != null) { + for (JingleConnection connection : connections) { + if (connection.getAccount() == account + && connection.hasTransportId(sid)) { + JingleTransport transport = connection.getTransport(); + if (transport instanceof JingleInbandTransport) { + JingleInbandTransport inbandTransport = (JingleInbandTransport) transport; + inbandTransport.deliverPayload(packet, payload); + return; + } + } + } + Log.d(Config.LOGTAG, + "couldnt deliver payload: " + payload.toString()); + } else { + Log.d(Config.LOGTAG, "no sid found in incomming ibb packet"); + } + } + + public void cancelInTransmission() { + for (JingleConnection connection : this.connections) { + if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { + connection.cancel(); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java new file mode 100644 index 000000000..cc1e92f62 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -0,0 +1,191 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import android.util.Base64; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleInbandTransport extends JingleTransport { + + private Account account; + private String counterpart; + private int blockSize; + private int bufferSize; + private int seq = 0; + private String sessionId; + + private boolean established = false; + + private DownloadableFile file; + + private InputStream fileInputStream = null; + private OutputStream fileOutputStream; + private long remainingSize; + private MessageDigest digest; + + private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; + + private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + sendNextBlock(); + } + } + }; + + public JingleInbandTransport(Account account, String counterpart, + String sid, int blocksize) { + this.account = account; + this.counterpart = counterpart; + this.blockSize = blocksize; + this.bufferSize = blocksize / 4; + this.sessionId = sid; + } + + public void connect(final OnTransportConnected callback) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.setTo(this.counterpart); + Element open = iq.addChild("open", "http://jabber.org/protocol/ibb"); + open.setAttribute("sid", this.sessionId); + open.setAttribute("stanza", "iq"); + open.setAttribute("block-size", Integer.toString(this.blockSize)); + + this.account.getXmppConnection().sendIqPacket(iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + callback.failed(); + } else { + callback.established(); + } + } + }); + } + + @Override + public void receive(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + file.getParentFile().mkdirs(); + file.createNewFile(); + this.fileOutputStream = file.createOutputStream(); + if (this.fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + this.remainingSize = file.getExpectedSize(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } + } + + @Override + public void send(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.digest = MessageDigest.getInstance("SHA-1"); + this.digest.reset(); + fileInputStream = this.file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + this.sendNextBlock(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + + private void sendNextBlock() { + byte[] buffer = new byte[this.bufferSize]; + try { + int count = fileInputStream.read(buffer); + if (count == -1) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + fileInputStream.close(); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } else { + this.digest.update(buffer); + String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.setTo(this.counterpart); + Element data = iq.addChild("data", + "http://jabber.org/protocol/ibb"); + data.setAttribute("seq", Integer.toString(this.seq)); + data.setAttribute("block-size", + Integer.toString(this.blockSize)); + data.setAttribute("sid", this.sessionId); + data.setContent(base64); + this.account.getXmppConnection().sendIqPacket(iq, + this.onAckReceived); + this.seq++; + } + } catch (IOException e) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + private void receiveNextBlock(String data) { + try { + byte[] buffer = Base64.decode(data, Base64.NO_WRAP); + if (this.remainingSize < buffer.length) { + buffer = Arrays + .copyOfRange(buffer, 0, (int) this.remainingSize); + } + this.remainingSize -= buffer.length; + + this.fileOutputStream.write(buffer); + + this.digest.update(buffer); + if (this.remainingSize <= 0) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + fileOutputStream.flush(); + fileOutputStream.close(); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } + } catch (IOException e) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + public void deliverPayload(IqPacket packet, Element payload) { + if (payload.getName().equals("open")) { + if (!established) { + established = true; + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_RESULT), null); + } else { + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_ERROR), null); + } + } else if (payload.getName().equals("data")) { + this.receiveNextBlock(payload.getContent()); + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_RESULT), null); + } else { + // TODO some sort of exception + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java new file mode 100644 index 000000000..1da2f0cdf --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -0,0 +1,212 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.utils.CryptoHelper; + +public class JingleSocks5Transport extends JingleTransport { + private JingleCandidate candidate; + private String destination; + private OutputStream outputStream; + private InputStream inputStream; + private boolean isEstablished = false; + private boolean activated = false; + protected Socket socket; + + public JingleSocks5Transport(JingleConnection jingleConnection, + JingleCandidate candidate) { + this.candidate = candidate; + try { + MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); + StringBuilder destBuilder = new StringBuilder(); + destBuilder.append(jingleConnection.getSessionId()); + if (candidate.isOurs()) { + destBuilder.append(jingleConnection.getAccount().getFullJid()); + destBuilder.append(jingleConnection.getCounterPart()); + } else { + destBuilder.append(jingleConnection.getCounterPart()); + destBuilder.append(jingleConnection.getAccount().getFullJid()); + } + mDigest.reset(); + this.destination = CryptoHelper.bytesToHex(mDigest + .digest(destBuilder.toString().getBytes())); + } catch (NoSuchAlgorithmException e) { + + } + } + + public void connect(final OnTransportConnected callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + socket = new Socket(candidate.getHost(), + candidate.getPort()); + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + byte[] login = { 0x05, 0x01, 0x00 }; + byte[] expectedReply = { 0x05, 0x00 }; + byte[] reply = new byte[2]; + outputStream.write(login); + inputStream.read(reply); + final String connect = Character.toString('\u0005') + + '\u0001' + '\u0000' + '\u0003' + '\u0028' + + destination + '\u0000' + '\u0000'; + if (Arrays.equals(reply, expectedReply)) { + outputStream.write(connect.getBytes()); + byte[] result = new byte[2]; + inputStream.read(result); + int status = result[1]; + if (status == 0) { + isEstablished = true; + callback.established(); + } else { + callback.failed(); + } + } else { + socket.close(); + callback.failed(); + } + } catch (UnknownHostException e) { + callback.failed(); + } catch (IOException e) { + callback.failed(); + } + } + }).start(); + + } + + public void send(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback) { + new Thread(new Runnable() { + + @Override + public void run() { + InputStream fileInputStream = null; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + fileInputStream = file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + int count; + byte[] buffer = new byte[8192]; + while ((count = fileInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + } + outputStream.flush(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + if (callback != null) { + callback.onFileTransmitted(file); + } + } catch (FileNotFoundException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (IOException e) { + callback.onFileTransferAborted(); + } + } + } + }).start(); + + } + + public void receive(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + inputStream.skip(45); + socket.setSoTimeout(30000); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream fileOutputStream = file.createOutputStream(); + if (fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + long remainingSize = file.getExpectedSize(); + byte[] buffer = new byte[8192]; + int count = buffer.length; + while (remainingSize > 0) { + count = inputStream.read(buffer); + if (count == -1) { + callback.onFileTransferAborted(); + return; + } else { + fileOutputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + remainingSize -= count; + } + } + fileOutputStream.flush(); + fileOutputStream.close(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + callback.onFileTransmitted(file); + } catch (FileNotFoundException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + }).start(); + } + + public boolean isProxy() { + return this.candidate.getType() == JingleCandidate.TYPE_PROXY; + } + + public boolean needsActivation() { + return (this.isProxy() && !this.activated); + } + + public void disconnect() { + if (this.socket != null) { + try { + this.socket.close(); + } catch (IOException e) { + + } + } + } + + public boolean isEstablished() { + return this.isEstablished; + } + + public JingleCandidate getCandidate() { + return this.candidate; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java new file mode 100644 index 000000000..1374e61cc --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.DownloadableFile; + +public abstract class JingleTransport { + public abstract void connect(final OnTransportConnected callback); + + public abstract void receive(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback); + + public abstract void send(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java new file mode 100644 index 000000000..e45e7441d --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.DownloadableFile; + +public interface OnFileTransmissionStatusChanged { + public void onFileTransmitted(DownloadableFile file); + + public void onFileTransferAborted(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java new file mode 100644 index 000000000..2aaf62a1b --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.PacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; + +public interface OnJinglePacketReceived extends PacketReceived { + public void onJinglePacketReceived(Account account, JinglePacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java new file mode 100644 index 000000000..03a437b2b --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java @@ -0,0 +1,6 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnPrimaryCandidateFound { + public void onPrimaryCandidateFound(boolean success, + JingleCandidate canditate); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java new file mode 100644 index 000000000..38f03c5d0 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnTransportConnected { + public void failed(); + + public void established(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java new file mode 100644 index 000000000..bcadbe778 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -0,0 +1,102 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.xml.Element; + +public class Content extends Element { + + private String transportId; + + private Content(String name) { + super(name); + } + + public Content() { + super("content"); + } + + public Content(String creator, String name) { + super("content"); + this.setAttribute("creator", creator); + this.setAttribute("name", name); + } + + public void setTransportId(String sid) { + this.transportId = sid; + } + + public void setFileOffer(DownloadableFile actualFile, boolean otr) { + Element description = this.addChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + Element offer = description.addChild("offer"); + Element file = offer.addChild("file"); + file.addChild("size").setContent(Long.toString(actualFile.getSize())); + if (otr) { + file.addChild("name").setContent(actualFile.getName() + ".otr"); + } else { + file.addChild("name").setContent(actualFile.getName()); + } + } + + public Element getFileOffer() { + Element description = this.findChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + if (description == null) { + return null; + } + Element offer = description.findChild("offer"); + if (offer == null) { + return null; + } + return offer.findChild("file"); + } + + public void setFileOffer(Element fileOffer) { + Element description = this.findChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + if (description == null) { + description = this.addChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + } + description.addChild(fileOffer); + } + + public String getTransportId() { + if (hasSocks5Transport()) { + this.transportId = socks5transport().getAttribute("sid"); + } else if (hasIbbTransport()) { + this.transportId = ibbTransport().getAttribute("sid"); + } + return this.transportId; + } + + public Element socks5transport() { + Element transport = this.findChild("transport", + "urn:xmpp:jingle:transports:s5b:1"); + if (transport == null) { + transport = this.addChild("transport", + "urn:xmpp:jingle:transports:s5b:1"); + transport.setAttribute("sid", this.transportId); + } + return transport; + } + + public Element ibbTransport() { + Element transport = this.findChild("transport", + "urn:xmpp:jingle:transports:ibb:1"); + if (transport == null) { + transport = this.addChild("transport", + "urn:xmpp:jingle:transports:ibb:1"); + transport.setAttribute("sid", this.transportId); + } + return transport; + } + + public boolean hasSocks5Transport() { + return this.hasChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + + public boolean hasIbbTransport() { + return this.hasChild("transport", "urn:xmpp:jingle:transports:ibb:1"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java new file mode 100644 index 000000000..77a736437 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JinglePacket extends IqPacket { + Content content = null; + Reason reason = null; + Element jingle = new Element("jingle"); + + @Override + public Element addChild(Element child) { + if ("jingle".equals(child.getName())) { + Element contentElement = child.findChild("content"); + if (contentElement != null) { + this.content = new Content(); + this.content.setChildren(contentElement.getChildren()); + this.content.setAttributes(contentElement.getAttributes()); + } + Element reasonElement = child.findChild("reason"); + if (reasonElement != null) { + this.reason = new Reason(); + this.reason.setChildren(reasonElement.getChildren()); + this.reason.setAttributes(reasonElement.getAttributes()); + } + this.jingle.setAttributes(child.getAttributes()); + } + return child; + } + + public JinglePacket setContent(Content content) { + this.content = content; + return this; + } + + public Content getJingleContent() { + if (this.content == null) { + this.content = new Content(); + } + return this.content; + } + + public JinglePacket setReason(Reason reason) { + this.reason = reason; + return this; + } + + public Reason getReason() { + return this.reason; + } + + private void build() { + this.children.clear(); + this.jingle.clearChildren(); + this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1"); + if (this.content != null) { + jingle.addChild(this.content); + } + if (this.reason != null) { + jingle.addChild(this.reason); + } + this.children.add(jingle); + this.setAttribute("type", "set"); + } + + public String getSessionId() { + return this.jingle.getAttribute("sid"); + } + + public void setSessionId(String sid) { + this.jingle.setAttribute("sid", sid); + } + + @Override + public String toString() { + this.build(); + return super.toString(); + } + + public void setAction(String action) { + this.jingle.setAttribute("action", action); + } + + public String getAction() { + return this.jingle.getAttribute("action"); + } + + public void setInitiator(String initiator) { + this.jingle.setAttribute("initiator", initiator); + } + + public boolean isAction(String action) { + return action.equalsIgnoreCase(this.getAction()); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java new file mode 100644 index 000000000..610d5e760 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class Reason extends Element { + private Reason(String name) { + super(name); + } + + public Reason() { + super("reason"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java new file mode 100644 index 000000000..154fadf65 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -0,0 +1,71 @@ +package eu.siacs.conversations.xmpp.pep; + +import eu.siacs.conversations.xml.Element; +import android.util.Base64; + +public class Avatar { + public String type; + public String sha1sum; + public String image; + public int height; + public int width; + public long size; + public String owner; + + public byte[] getImageAsBytes() { + return Base64.decode(image, Base64.DEFAULT); + } + + public String getFilename() { + if (type == null) { + return sha1sum; + } else if (type.equalsIgnoreCase("image/webp")) { + return sha1sum + ".webp"; + } else if (type.equalsIgnoreCase("image/png")) { + return sha1sum + ".png"; + } else { + return sha1sum; + } + } + + public static Avatar parseMetadata(Element items) { + Element item = items.findChild("item"); + if (item == null) { + return null; + } + Element metadata = item.findChild("metadata"); + if (metadata == null) { + return null; + } + String primaryId = item.getAttribute("id"); + if (primaryId == null) { + return null; + } + for (Element child : metadata.getChildren()) { + if (child.getName().equals("info") + && primaryId.equals(child.getAttribute("id"))) { + Avatar avatar = new Avatar(); + String height = child.getAttribute("height"); + String width = child.getAttribute("width"); + String size = child.getAttribute("bytes"); + try { + if (height != null) { + avatar.height = Integer.parseInt(height); + } + if (width != null) { + avatar.width = Integer.parseInt(width); + } + if (size != null) { + avatar.size = Long.parseLong(size); + } + } catch (NumberFormatException e) { + return null; + } + avatar.type = child.getAttribute("type"); + avatar.sha1sum = child.getAttribute("id"); + return avatar; + } + } + return null; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java new file mode 100644 index 000000000..eef41c791 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java @@ -0,0 +1,34 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class AbstractStanza extends Element { + + protected AbstractStanza(String name) { + super(name); + } + + public String getTo() { + return getAttribute("to"); + } + + public String getFrom() { + return getAttribute("from"); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setTo(String to) { + setAttribute("to", to); + } + + public void setFrom(String from) { + setAttribute("from", from); + } + + public void setId(String id) { + setAttribute("id", id); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java new file mode 100644 index 000000000..9df05e678 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class IqPacket extends AbstractStanza { + + public static final int TYPE_ERROR = -1; + public static final int TYPE_SET = 0; + public static final int TYPE_RESULT = 1; + public static final int TYPE_GET = 2; + + private IqPacket(String name) { + super(name); + } + + public IqPacket(int type) { + super("iq"); + switch (type) { + case TYPE_SET: + this.setAttribute("type", "set"); + break; + case TYPE_GET: + this.setAttribute("type", "get"); + break; + case TYPE_RESULT: + this.setAttribute("type", "result"); + break; + case TYPE_ERROR: + this.setAttribute("type", "error"); + break; + default: + break; + } + } + + public IqPacket() { + super("iq"); + } + + public Element query() { + Element query = findChild("query"); + if (query == null) { + query = addChild("query"); + } + return query; + } + + public Element query(String xmlns) { + Element query = query(); + query.setAttribute("xmlns", xmlns); + return query(); + } + + public int getType() { + String type = getAttribute("type"); + if ("error".equals(type)) { + return TYPE_ERROR; + } else if ("result".equals(type)) { + return TYPE_RESULT; + } else if ("set".equals(type)) { + return TYPE_SET; + } else if ("get".equals(type)) { + return TYPE_GET; + } else { + return 1000; + } + } + + public IqPacket generateRespone(int type) { + IqPacket packet = new IqPacket(type); + packet.setTo(this.getFrom()); + packet.setId(this.getId()); + return packet; + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java new file mode 100644 index 000000000..4e7b532bf --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -0,0 +1,66 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class MessagePacket extends AbstractStanza { + public static final int TYPE_CHAT = 0; + public static final int TYPE_NORMAL = 2; + public static final int TYPE_GROUPCHAT = 3; + public static final int TYPE_ERROR = 4; + public static final int TYPE_HEADLINE = 5; + + public MessagePacket() { + super("message"); + } + + public String getBody() { + Element body = this.findChild("body"); + if (body != null) { + return body.getContent(); + } else { + return null; + } + } + + public void setBody(String text) { + this.children.remove(findChild("body")); + Element body = new Element("body"); + body.setContent(text); + this.children.add(body); + } + + public void setType(int type) { + switch (type) { + case TYPE_CHAT: + this.setAttribute("type", "chat"); + break; + case TYPE_GROUPCHAT: + this.setAttribute("type", "groupchat"); + break; + case TYPE_NORMAL: + break; + default: + this.setAttribute("type", "chat"); + break; + } + } + + public int getType() { + String type = getAttribute("type"); + if (type == null) { + return TYPE_NORMAL; + } else if (type.equals("normal")) { + return TYPE_NORMAL; + } else if (type.equals("chat")) { + return TYPE_CHAT; + } else if (type.equals("groupchat")) { + return TYPE_GROUPCHAT; + } else if (type.equals("error")) { + return TYPE_ERROR; + } else if (type.equals("headline")) { + return TYPE_HEADLINE; + } else { + return TYPE_NORMAL; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java new file mode 100644 index 000000000..7ea320995 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp.stanzas; + +public class PresencePacket extends AbstractStanza { + + public PresencePacket() { + super("presence"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java new file mode 100644 index 000000000..78ab66d8f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.xmpp.stanzas.csi; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class ActivePacket extends AbstractStanza { + public ActivePacket() { + super("active"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java new file mode 100644 index 000000000..f109280f1 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.xmpp.stanzas.csi; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class InactivePacket extends AbstractStanza { + public InactivePacket() { + super("inactive"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java new file mode 100644 index 000000000..f93b5d870 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class AckPacket extends AbstractStanza { + + public AckPacket(int sequence, int smVersion) { + super("a"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("h", Integer.toString(sequence)); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java new file mode 100644 index 000000000..78cd81edc --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class EnablePacket extends AbstractStanza { + + public EnablePacket(int smVersion) { + super("enable"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("resume", "true"); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java new file mode 100644 index 000000000..98cfc748b --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java @@ -0,0 +1,12 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class RequestPacket extends AbstractStanza { + + public RequestPacket(int smVersion) { + super("r"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java new file mode 100644 index 000000000..9cdcfa5ec --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class ResumePacket extends AbstractStanza { + + public ResumePacket(String id, int sequence, int smVersion) { + super("resume"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("previd", id); + this.setAttribute("h", Integer.toString(sequence)); + } + +} diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_add_group.png b/conversations/src/main/res/drawable-hdpi/ic_action_add_group.png new file mode 100644 index 0000000000000000000000000000000000000000..97640355484b5f5f8ad1e6a0a7b3e10fb8a10fe5 GIT binary patch literal 876 zcmV-y1C#uTP)RK++puY0p`9?KzxS+a04!2U4Zw_N^ie;dJ}s*wg;HJMeofs*y9;}jr1jj zJU)DLdAtL-v3Rx@Je^JR+;Q9dah0-~`ZyM%tTpmm<%#?Pdu>~W2 zIRU;;V~=@9)IwboVS;zxvEH(&RKZ?hr}_qqe{wRz29dZQ(uf6sJzUGU_!I9=gwYZt z&j;*Vft+_PC0Icmov--nhL~uF)7grkFHzg%Z6{2Z6}}#5{u|bcYw9X?)pc9F)Rc(> zRz~tKF9b_-_AK!^5g^Y18~TM;L5Dd2m%=?{FqhT-#sHxBQk2KJ2p%69xY}TcpbHcb zl_Hto_KA|c`Pm!ZS&3(vAcPS21D#>c01Qfq+N?~AzSIEM(Pia=tl%%hCP)@{>m~qQ zYWo7#Kgd!TNyd=wmWD=UjZjl#8BwLRmU2K7 zf3YUtb3pk)AV!ADPcN)I%d4z;M7fd-1^}MFwD^7gSTHiM{``uK&aAEq9(n>#SMJa{ zx$4;PXtOH<2(1E8GoNcRzO@2KjCE3#7NKTN>Nx=5IN0`eKn&&(uNfd2fCU(! z43G>!2O#fF-;)8FzBdE31ElXs-pc|EkPMIkQd=_82>}P3?+z3!SqSC8{`_y^c@s^f zMS|lckH;gDZf`<}>)-)^(kr+mz+uJDH{dhyk7+QXhvKO49(dm3_?9nR_sPfE#d4*&>GTE@JJHm*GI$|NtOP_zdC1TL7YUr=n@ z19N7thpv*Ci)Ld7;Ef$X&iS%H+3AGHAd$-o+o~ARfIY6 z&XL@{B0;x?LO0@CFa3skzcpMBmgR0Pb@@ow0~_weqlVse#|yv4`Y#r@vzR-CVoTO9 zDA9R1s9EB(v8NO3a%W%gCVDpd7hnK{lIR`?0bJAo0000~)nSPLy$xy&`@eRi+4`$0u-i@ZSFG}rM%CNWU z-s*Jro6RhXH|rcc`Ad$cF){L=f1CUx8KC9^|2Jt#8@lOto3AQAVadO~_w4NcvY?X- ze$2kIFlU|h%qhz&ZvL(AXIu5C+TcUro>J!cqtRvygz|4Tc-82-dHa3Z{8+}cX#1Px zE}O$@{0(=-FwYY7GGgi!{_&7A=6IH;#OmJ0otjI0RvzF8y>W`K^_W$SH}f=kiTZ%@CcWlz>SV-&cbcz5(=I`I&}eQ^}=yu%B}k<1Q=1Y&R^aMFu2pR zb*^YXHOHNPiGT=qf(r%bLeks`5&{7vF*7I$4>X*6E^A8mTr>efJ@E0d;v+zB*!|Z$ zVwF%Iaw4unkkHT?NXVyz*xYC>30bibh+RVXkU>HtB;<TUu!n(oHu3^#E4{%!fDX5>*93py`ZNJ;{oaQjCpqb(_(Z_$LV&4}NbF0082~ bTYv!o)Zbu6Ia}sl00000NkvXXu0mjfJR_Q8 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_discard.png b/conversations/src/main/res/drawable-hdpi/ic_action_discard.png new file mode 100644 index 0000000000000000000000000000000000000000..703b31f8027859b5810937a5c2da2b97428c68ed GIT binary patch literal 450 zcmV;z0X_bSP)3kAs1qih2LP}sp1f-fdKLlLI zKM9>014mq!#s;4VM$Y&&F`b*7>b5h szZOL!<(tn#I`>~o3WY)u2Oa_p0K4L5a@AHer~m)}07*qoM6N<$g3So4vj6}9 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_edit.png b/conversations/src/main/res/drawable-hdpi/ic_action_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..756db316e07cb4d6f04db480697de524d0bd1034 GIT binary patch literal 765 zcmVx z(n%-(wFGSdaXz1i7#7emr}of~APBP769D)KLjnyiEDMppAaW{tzwa4*jA2~>UStG) zBgu9V0H_Iu2a89A+zQW!tso#kpt7@jxW*jg4EkR2+MtMR;2J#rUAcq+PeJ5BH(3$v zB@x1pzAX?kFjY`ap|1e{0B|`zi$(H+cO$PZ#2x+vpewq~MkDov^(C{-qejroNdB@7 z^plz~`^fwVy4BW}+We=XBcB#HXn`s|{tQ3~-9_K=sgQ#u_C{c*oO8bO3#Q8-H-fGW zYeZCKi7hi##P2V@+-7M2os_JFhTAOo+`1_L-_(WZ%TptdzYur`caZf|Uo+fB`Fm*_ zaT5X;!0RIO2}z_nU3atM zk6oy{Lw&>GGp80{JJ1^NzqP=SrCwTqxa0(NbMUpY5P+-dcw+iXF?W>K*cW(f3$P>H zYEwdDCV9I?Of(L@P7F*hoNb?3{`Jn%Dfl`T$O?R2y2%wSF=S6Oa0TAV0xa``63fV; zW5Q1I;1ay83&j?fPAZUdm*6W2FnDbOG~~5O<`TS(1?m*7IopDnEATc1V$0t-s*5L^ zV_=hVS8%qHdNgg7?rJr+8F6bBgDqJXES!0PfUkwTBqa zF{?w2eS)kfkhongmouvbIE$k9=h6VaDms^7#dw18QA{lSH4yM^OXw~GQw62Oz&VH= zQ{xwCi!B{{^ zFyE0F5J}F^dHbf`%g3y0Z2?3A1bg1af{kP z#xpOg>jEzyYce{~fXR&ZIaZ6=iV~C6`J5|qKkgls48ix9=bwsfII}az;G;IX4l~aU zHQu2oZYd>^*E@XYw^0HF-wx$*AyO6H6P$Rz!F!KzZJ^6WQYJo6asB7IcU264*G<43 z0oYdnn`(ev@eq1ntnS)Oxz~D`j_Z{wceUsqRcwm!7 zOC3(VHU)m41Y{@mjaex$Sw= zWEdp>ISnF!2p|H803v`-4d6?NO)ry$g_?V{Y@n!*#(nESo!9&fQnjRZw0#o~t zn|e|{qsNkXWVz&pxF8Ux4?hD?zH(4L!Q2XKKh6UPn_s$+u^ti;n=JOuPhGo_S_sS3>L|NNdS56hPAgEq(0?2kwA%XIOKr%#mK4;Se8S09DotWbHcv(1tVRaCQ+}dkfHoMS){wmaGg~ zgwjD4gIH++w8V5*TH7HE&Por}^fn`*^k23xgH=Lm3eVWqfw^q%Q~+zsDr`Jh|5(Jz zmS(QCW{}*ut`5`|1!~t=QQU)@ZgCAOpAAhSd*=s+w&H%gfcGX>xsl0000~)nSPMBK|z4!;0>128*SSfO(WZG({C@e<@zQ{J^CA$J^9^h-4+HW z4uu9V;m}>FaPIT6gtre({HFI@`KRUogk{zopr05h*# A?EnA( literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..c01c2b3822af20897f596c907282761e7e8a085f GIT binary patch literal 587 zcmV-R0<`^!P)^*`+#ouZTE6t`vL8}-M#&Q@d4!n`T@+|9LYin6euL9v1BE(BY1Wk$ByGz zvSew{g6atT28NuBHGse`fjz|%+>NI;MvQs{ z{{cYnV0-{hNQSzFaR&>*1Mh$j#F1o8%=Er7J0S5q#`#9Fbj{|W5eC!%?|>ZRzjY=W zbq$`vDO9W%p_3UI^96aNSdXKpZ3oIcFH4~k`6q>wv7WD#TksZqGkFBOYIA^fgBP;* zA%owwDI+X+3%&;fRO&Iw6124SrmryBC!r)GF(OE~QE`1W-7CmefKK z>$b4gnX4LkYWV-Yd$qa@Zt6r@v#1HA5 zo<<&lr*w&>z;g5&^cW&@zipAqrEDJ^2}144K(FHK)8=3fRA3&a)> zt;_Ql9ujbPW4=J7Ft?K-kJUL{v6>q^16Kp!1kCf~$CMF0XgYUw!tX0*ljrvOx6FIhtW z4Di*z5I*}EhHPX{1xmAE2@!w*1Rwwb2tWV=5P$##peTx>M%7(_0RX3XJ>7+I`se@v N002ovPDHLkV1nnKjOG9U literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-hdpi/ic_action_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..45b22282fc9a8955714ccc581926615b21ab9f8a GIT binary patch literal 678 zcmV;X0$KfuP)PBh z(#*x-0~LT~5_ps`Xg351_yTk;?Y2&)sIWeU@nE>t4gq+%fMH2H)>c9Vcs!BZDFFa~ zPvB2UNm>UY$Tva;DR(EJ0iS`UVB1B*dNiMvt)MZo1RkAo%ZwgV&HafJ0u(J`#VrJ$ zGM}-eNOcwyiWLt}Fcew@@5Y44ZiRF?H2@#iz?YghpPLXEFafM30wsXoc#+Hj__1R5 z6Q8nCIck)ueCCn~2t~;Bsvd}ZCm`oK-yPNnf^sfqrsF{>3N z_;I_f&Hus(X5yeO!{{IIiWI!yBx2{xTgE{z-E}2;0zH9iCh#M`04g=hy&bZ$h5!Hn M07*qoM6N<$g8Nk_lmGw# literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_remove.png b/conversations/src/main/res/drawable-hdpi/ic_action_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..58a56e457b0bc39aade617acddcf52b501d495e7 GIT binary patch literal 448 zcmV;x0YCnUP)P4JAQ8dR_a&lVlZKy!ug}vT`lWF+@e-?m3aEez zsDKLml|Y)NTd8w2h>dAu`+jFU8`;ZcCv}}9$+52M2MS&;xzvrsT}s>&T)_3t&OI(r z5)Js3d$p3dcZqvOPoYxkrSWVV%tNMA3w-3-DB&Yud)oH6?Rhr>K_k%tUsP3fqpSgx z1Pb0;03m^dA0!YWfrcL}Fhqg@A0*%-!GgDC2HuEX0Np(W9wyKu5wYNdr_hJfX~BYr z3Uo-&;C~{WhK|6Sbr7KwK#D-eBEBMWC#pctZ?r@ufE0ngO$wdGLIpnEVVM$!4bPS^ zW56>d%vkU&2{R@Ao#mPCGKjsl?eHI=Kd5iCN1?loIzqOP;>% qXmRpIkJnW|1yn!17hKoI8QGX!~*m^_#PXe04cx%%--ZTFyA14Gmmwu;gv|$2 zxQ0c1*h&;@xVvikvml365O(6)A?9dQQ(07}_)*z$d0h1^~N?6?9btxc>Su#Z)2YWI$PL8H2%B-;qg5Cv_Au0jcmS*c76rF1PHD=qcO#5~|BiWF^iQr6!iVZ(}^ zM;hb3iwUjnM5gxke7I33GBwOD%Ciif^)Mr(y#ruCmdd1ci*m&L2_CEpy-ky5vCXMO z(Gt(lm`xSyN=~_Dv8PiI!Wo#5)X|M4wLqBYh3-|a6@i?O8Gud;p$$S90N_BmmB!M$ z;ba6rO%RJ)hhfI%0a$jUvD6)<2Q)n)4-#HSJ(1n<#Kw6PlAcfwzJiCFZ*h9(JO7F7 kpL`a81z-V~r5^zX03bgQxSV;wqW}N^07*qoM6N<$f~DIU2><{9 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_secure.png b/conversations/src/main/res/drawable-hdpi/ic_action_secure.png new file mode 100644 index 0000000000000000000000000000000000000000..4439d1aecdff690a389e5b19921b0bd78b0ab784 GIT binary patch literal 384 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@j7pv^jv*Dd-pshj+hicnw({dt#sUVh1P1Yd`=tyL2UsHx zFqSaXhKm(6=sjRCa9}@>9VA>NcyNou;{Wqw^mGH>Y2TSD#3I1rc){MA$Fv~!y5Y_X zCQ}6-J+x(D-K#KtlCJc@{Yy@~OHpsq+{?V+!S*NLZ2j#8jvSS4&~Gm1Qj}qtKf!vR zrAlw6>aiV0O%u$%&$q5%(9C|Fa<=l)tR?48CY9xy-4r(PtWLeVds|ns!nrlo$&;@b zpT5~2x%)?m-r0?dl6yQEf9*McNu6QcrHhu68GG~?7!uA*o$>0&tUIeNF1_B^p_{ZS zdB%j$d0nd8^p0*{81`tz)5!;xskr3`sT2Xj2?=~Caz1Qs;Jl*f=Xa?l1&&|2wyY8i XlDAzMbuP@F22$tg>gTe~DWM4fdvcYL literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png new file mode 100644 index 0000000000000000000000000000000000000000..505cbe63abc1a4af6e3dd2eef57a82e53edd0e60 GIT binary patch literal 932 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 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png new file mode 100644 index 0000000000000000000000000000000000000000..48676f7bdfd6584d541b65a8d9768703f483e953 GIT binary patch literal 1095 zcmV-N1i1T&P)Bzu(8_v{Ca$>HtFh0dee$K?++Xgir^I z0Wnkz3{0RxLmL=S2WC*<4`5&@NBO4`3!uY^QOBx-mo$5hcFoVMtyR$4PS+ z-#N}_jL*+~KDzhyogL0Bl|NDC?~S{S7S17J5TV$;nWdTea4{^OwNP3ZJ`CSMtRHv( zVzO>#(RsNdEI%l}DgnOjd*z&j(994nhu;%+>Bu8SJcMS3>T=~X17=@(A@&snw zP}Qh)uhqzMuBRIS0O3;jHEC0MoG7o5o`C97rnELPt%iNDBV@JX0) zLyH=(#TEP&v^h6ypSt%2508`2X10O4|YpRg;L*rnjKAx8y#MA&rwgbxWi9#u}cBLb|*ndU%>3+*@>Oa?eB86OTkGYeE|Rv zJ`CR#)yn`KNT+}U;HY+Km>c{c1lwX7tsDwDGT_)D3zSNwG9iD9gTR3uN79*MQb`9N zO1jfL*(P#OJQrHsholNf!|dbPvH0KELE(TMw`+9l&_OT>z)|S{brepRnWUZXLxLRK z0RUTDTM}3xvMmA$q`;1V5LFVXb^Fl*w>zoo#6A6+m~kmLs{|w!_w)rQSmGHh z8yXq~(fS_=#XUU%*MGV`K0a~bwQSX1PFH*`ow%nbVBC#Q0XmdNg+awVJpn{qvYa1v zC}_w|F7yu>}-fK6<^&J2GOBG8neSYNrF zkGQ8BAk5|lHY{<^fjuzT4Q$xro&bRVdHoGQQx71}B!cMa+|y^7+a(|XfXda%m9*cW z&RX2FcLm64B>Y}5D&n5K0BGq%eM*Zvj8?x|UC&=DIRQylmMT-n-oQpw+!K@^lwUdW zf5am$?g`|Aln@{I^r5sUj8;D%ZE;V47}EgGowz3;R8c7Wa^A!}{{e3=9&B8Du*LuY N002ovPDHLkV1kGt>@5HQ literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_activity.png b/conversations/src/main/res/drawable-hdpi/ic_activity.png new file mode 100644 index 0000000000000000000000000000000000000000..613da683aa83c22f21816a53ab77069cd2208616 GIT binary patch literal 3040 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 S!sQPD0000;5spCy@rKA4@e+EicBPe8d-r{x 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 VK7p!4^Hu-=002ovPDHLkV1n4Q%TfRU literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_received_indicator.png b/conversations/src/main/res/drawable-hdpi/ic_received_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e3f27486a20085b651510660b0a07a5cf751ea GIT binary patch literal 686 zcmV;f0#W^mP)8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10xL;GK~y-6tdah+q{2p_ba> z!5;j;lZRH6g5GLx0Z|VggenLgDu@RWX`$d3cu;H(6-7NL5wR3O)RaKdCe5bl?xxx7 zy5m7pYHaf%4!p~}H@}(p7(yw<|5)t593nl$79I#+X9Q+HEKE>a(56YVFY+AmgdF=E zlbqk$4*+^rkL#bxLrp>6L2tl*z!BrT+)e<{zLxGQ@0BxBOe!!9w?l}9-z(g%AjOaB zL~=ZNLW)TR0B9HroeN#&J6jF|fcCxgI5m+PqZ`_32x>nYJkIa1MFGIb=qjo$Y4btjLF3di*2)3zbBmFL04-x9$RfQc|0g9!jrkjev;#qQ`x( zZN}=i?x8x>mAD+$zG-pyv9@Ox!6MKf)XYDZdUKC*Z)-x7Qh1JfB4VF&hHYiT0AQ|} zWyZ!>0YFceUnDLi25KR|?_n=LS^lhikWZOKa}@w8$S+8rs)wvY0Fa%^&KBe9DAlP^ z*)vv*#nj8xbW;!jr0=Hh73b8bAsGo%HRW0beWxO!dg&2n4CPs_*$ UM9b_%mH+?%07*qoM6N<$g0yQUzW@LL literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png b/conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..2a2934fb1f6ab2f8b92e34c3760c13e58a8fa8a8 GIT binary patch literal 294 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|wj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&kwQQ$$2T-c%!eFHmT+r;B5V#p$969?FCFh(C$pS)U$ ziPK`$1Ff`7&w1-7G%i2aov~F*Dq(^pv#r39doP;z$WLrwHeg)5+i8l~$;p4%Q-GV+$~ngoLiuFDO^1A5YYJyp00i_>zopr0F~o!Qvd(} literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..b8f44c21ea9cd6ef5d9cc62acaf87ef8ee4a46f4 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1i!3HFsuehcLq_jL;9780+lT#WR{s&087#caT xI+*bMQTLtj&|cuc7b7NLmf5BuEEkzs7-nD7VNSI5+yvCc;OXk;vd$@?2>?!!8Fv5x literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..5512dbd3061cfeb970bd17c37cd5f9d56da74922 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1i!3HFsuehcLq_jL;9780+lT#WR{s&087#caT x`b=OsKSAxp2YHT!Tat}xOgQm;OXk;vd$@?2>@dS8Os0w literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..e5f1df225b64b19b7c33ef91c1e41f7fd9fa5005 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1i!3HFsuehcLqzpV=9780+lT#WR{s&087#cOP z+}mG&pF>zgL}ZP+?}Uf;0tdDjGx@S)${RJkU}R$OH8f>@X(2rasF%Uh)z4*}Q$iB} D3;Y{< literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..7cd46d63d760d8cd6bbb154e9b1191fb373aac23 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1c!3HD^Kbl$tDIHH2#}JO_+ y_x9J{=MWYV5m}?|JArYN!0CyUdCLgTF6@F`C7RLrw<|qKZzb@&zb?M( zhSTXZpvx0!h*uS9?)!e-DFAZJ4yUBCVabj_EIR>!s6Ry1CMifWmuv)<>!udYS%#$P z-5H=r*}#~zPs&jo@M+AP)McjEaK3It`h6$4nWWFi$oU=lcqI^}sRT1k znoDHERR-XLlu%cmpi#fJM3qqY{w@-_N?{vNPu%B7-T4^d_|+g3U+0F%lqBdA44@!8 znc&z$QuC4yiXPEQ|>)iL~{J%ttk&@Zv60_qT8YGjAyO@J%#O#$Y86CA%p1mKA^{QNHa zfsY>ld8N~_;Vo_2VQqrQ6fX*7MK!Gyr8C)MZj2>>1ju!=;UR;2E<;J1j|L8K3|t14>)yh!ZgfA{{Y&s5w#9Ala{$~(PMnC z17X66HhfJsT>;Q#V1jlp=Rwu$&p9=hWrRH06Zqp Ugb*)q$p8QV07*qoM6N<$f@*LWg#Z8m literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_add_person.png b/conversations/src/main/res/drawable-mdpi/ic_action_add_person.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d8f46a9773894f19219cc1b4adee0aaed0478e GIT binary patch literal 469 zcmV;`0V@89P)5twHw6_%riuPs0{z5rfE zY&%b1TQ&x8`URG=w=*GLxfqZkrvY%unSu~mZ~^E70eBZ-`XHAL=xpik*U)O>)at6X z_^)H*5&(A43-mmeTi8n4m$Ub2I6Y|~Him5peJ6Vvs}m0NuC>}2^1Pz82K8|O7+o=^ zpc#QsT5$}d(R;0^Cnjw6CDn>#Wq=$O8qMG1mo(8*VA|DCy3GPW!H*$Lg=%E)Gn9m- z=)MAj|GW_ZASZtX^_36TPPmyFTuG)~SvfUVYyddKc{Ha6{hz<~QR>kzbSY>xYoHnC z1~-6<B3Edx+z`yTT}}1B}lL?P7vuSInZ&&(9G=H(&oAM&&{*lEq*M~ zaq^diTc0}IY1cX6a{K~YG~buMj~fdZ!cM<@$}neIvNyBA2eFR+$qAhMwtQxp{XwnsAeYfn?40oMn{1_ow^&HFU(vx!X50Xmt%)78&q Iol`;+0ER7O;Q#;t literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_copy.png b/conversations/src/main/res/drawable-mdpi/ic_action_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..7134820209a2219c193c8e08a48c0e1a52aa3368 GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1Ghdf;zLn>}1B}lL`^GIyul#-S%X4I7AI$Kxt?aj^n7Lkn) z1lS|vd=@fSHU1D&boF7DWz}gBVQ4m-E5h7((R}hMJ_TjpFG??im8YmhbeK#@%S`&P zoH1&7CnJC3LUGm$38HL4VHtF4>9Waur;s5{tYCy^X zhxBqrM#j&in8!kfqlTV4U*;DIaLCs{^ql z0WHsg_$Cma1>!A)(gZOM*bk(5$qgl-0~{%h0$vJ2n3o!9;4B3WI7^js0GjvF(|{Bb z8x-%)(9-}g_-|3Av;sQ7msWa+4S_QxMhCc{0)~JIwF@F+YS#r!3igP)o>-REzS-HgGs|dn zw;P)^)Wmt7J9xN!JzAEPT+IQH3-O!QeFR?NNH(YgAouWCSA0GJE7b|xAn_RR(L4js zz(>ij8L&`0Y=euJi9Lq?ogrTVnKtgxxC2=EoJ}E&feD?M%D!vEe~fAX~%p_a@b zW+u2~K@5Rlh>7VL%q$lE1uV0(rw~J8Fo(dL*^(IqGZP~jW`cRZI-b0sc zlU7z=axKk1V6_WHT<&X@J+1rHssn%vNDFF1SIX=i2BO%CSb@jZf)H>4IDqb;7w8xY z2#r{;(jl=$U7Ui26$c2fGtf?<2_30Y;@G{jE@nLf{7oOn-4ZU%=W-u04wl@ONOk~x zY#OUz0j^rWSr3>6N6-`C)1>iSi*RXeF28`zSHUx-;*@Y{eSo7h9o-z@4v(T^#eogr zA!a|a;sD@fLq1;zkF&&8VNHe;=lc=5hYp!qt-)71fSc_cMU%(r8+za|^Am#Zd6S&t zuDRW$wYU{GYI32)X(K+h1H9ez$&Y3hmTvXjrL8ianoE}8hYG%>F^1Yzz&q(4&M7}7 zn%NFcdK64Jx#PL=9l95Zo<*D$a@a=z0y&_AD>;3D@z}1D#$?~xc!KnZ%a_B)H{u*j zlJYE1Fhcd3R@VrW0i6x({Qn!yYQF$BebyG|ET5t$l81PLPKHCEz1Rav?1KymyBL#^ zD9r*cmY`ZVa4zW;Xz3)i>f0bNBzuFcrRgz+zuE1(vSLIB-gC!*iSHWU^e)t3_$ k5Lgz|EVIn=Enfl*0OV7p^G0z|2LJ#707*qoM6N<$g5*NzJpcdz literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_new.png b/conversations/src/main/res/drawable-mdpi/ic_action_new.png new file mode 100644 index 0000000000000000000000000000000000000000..f17e7980ee9aee545cb23bf93e9b21613278514c GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G(Vi}jAr-gYOn2mC5a3}6{c~dRo|E${f{)I<>^0$x%HC5M zDbb1y96PG#E}b*crF~7D(!F;|OdJXg4Qn5Lbvcy#A}ws*s|Kr{TRL)zK;aKt8+M77 g+o>-wVv%4tbz9`!VZkj~Kx-I0UHx3vIVCg!07CUbG5`Po literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..1d265aac640c1150acfee6c0c4162002c6de093f GIT binary patch literal 415 zcmV;Q0bu@#P)pyRRu*@TMcTIA zL-|1Cy6$sP13-R(I%rKbaE2kxnEBpJ_Q+ZUgy*RU zcfmTeVt^zE5>7%TN7gUvVb}gM*}o*Y@&9khL)P({&6yHf4SeSGu$S1}nt;qAhQ z#k+dWTTyV4$>$>P%K>$pW%?fS_pIc-FCy{EsadQ0-IQ>#conJfZ+R}1B}lL;%Nk5t{qg_*|5*-Q;-~(eIdP(*TaS++ z>J7t^uEq!<-&d9b-*b*D7(2~p?S86V4qdO6~@JU5~U0~T{nto s_HxTSu-u?gXLgHOiggErLjxm2+i|C-3qI&R2l|V_)78&qol`;+01n@4_5c6? literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-mdpi/ic_action_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..de008e51a306771d6820dcd24871bb30360e1525 GIT binary patch literal 507 zcmVJNRCwC#SG`WdFc5B07qXg>tYM9m ziH_{7U7Kg18{!q<8N%9?u3g(H60BL>HEUQoqpNf}e0J?L{HZ6M>?W7{?$3AbXmq0g zOhr*BpUm07;152zaiG!!mTTY)IU%n#@D6x`>m6_k{8-m@Bk7z3fZxJ$!$2ve$i~3V zAs1!pT@B{ zmb<_k2gfVe)N*O5T=QUjKn*NdC3EB6G1Ru>R(Wb9I0Y>ON(H&b*7afyXfOgs;Tl`( x6+Tl;&H!%vw(l>OQLO0S`{&|Bf0{l77yyZQf|ZLckgWg!002ovPDHLkV1gTK=LP@( literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_remove.png b/conversations/src/main/res/drawable-mdpi/ic_action_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..342a79de6b7d3f18a7dbf7b97b6c8944e37d575a GIT binary patch literal 282 zcmV+#0pTpO6{U?C(jk_j4kWfkngEf(U{J>J_0T!D@Ue32A%s&x%(XKg8;9rG zqm(yRc%t|ei4vhyd`cXI8rV55iHS`7N*q`VN`$3$NOGGEv=SrY%?4@$sJO^LrN5B4 z*g(Mqq2iE%7ZZevw~V3gChV>Ss+Ul83{@AA>MBMEr8TuN6u;7x^Z1JX*#jQA*_jD% g|9u8S*SG~30I0v97|?&!(*OVf07*qoM6N<$f?e`)$^ZZW literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_search.png b/conversations/src/main/res/drawable-mdpi/ic_action_search.png new file mode 100644 index 0000000000000000000000000000000000000000..4edb1ff92fe4c88a984a299ae6ff643cb6e0cbf2 GIT binary patch literal 449 zcmV;y0Y3hTP)6(JFc6ISQ^2_Z8gK&? zKnh3$Pyh{301bo!NCCP3<^H4r7XSsgfG{hqB`d-vmX_Q-veIaUz4DA5@0g-kVu?Qv zHKk}628~n!8$k2$9q52xrPTdn0ziIb@>n?F0uhb40Fc*6hJO-43nI&NG+qKbp8v+& zN$lIu&Vd{718fp?hHI#{k|Ni<;4MWy65++JbGTNKalezsA9?aSD%B$qhxy)G0`#~?&hQ7DWg!+qj)|D%1X3cJ rV$Ps%4xE82Ipy;Ie~Et^PXPu1!=KTV_gtK200000NkvXXu0mjf`(nR7 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_secure.png b/conversations/src/main/res/drawable-mdpi/ic_action_secure.png new file mode 100644 index 0000000000000000000000000000000000000000..05332ebfa7e867bfde5f59d5d0456ae0d4eb4afc GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1Gmpok@Ln>}1B}lL?PH13M-YNC}|NrycORg-q6W!C(Gymly zh6P6zyt^B3Ncg_e5%|s|ThyJ##}LK0X$yNtWuXRRs}tu1!St2y7&0d0J8k--BVz2} zd|2Y7VBHfpPCND=OdlmR@(w87Z%}#fy=JG6$__@$bJZJ9e18<+ZOZO(=rOPNhee&9 zmku$SFfzYm5M$!>=hC#Oytql(?m+jFXO;$4^CnK^ZERb@xYC5n){^^0L*(Mjge#1T z`6NmYtYAK&+qKkVWmF-FCG0p?&92J4fGv@r>mdKI;Vst0Mg-b AWdHyG literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdca901a5d8443e450e352629fc618bab00be46 GIT binary patch literal 650 zcmV;50(Jd~P)fmYp0YQc427$HC8vpC8)npkh5;e>STMV=E-st>=T|28e&?4j$z;N6 zu<#1bear1{tm8kF0YF%@oP^PGGC996XaJL7;jxI9^&<`&aA};YmP-Qx{Fsh%r%vyi z&I|}bNvMX=M-pRKEpQHHtbH1T`(fD-oMfs%hJ9$g4f~4Y?V6U{=1h}>q7l#ji zo%q%p@y|@^yVTth%{kT?pOi1cr#ME+Yt_nu+md3F#a=l)w8x_(_P0_)IYVm+fB;x) z)NVry9SzevYM8*m-DoB~)vPgQtTuW_;~OU*#2AR-Ea0n(z^Z{4-`QQ=27rb!=@ie6 z;XEM7oG&*}`L7~ literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png new file mode 100644 index 0000000000000000000000000000000000000000..7723f4aa94b7a5eec5fc5baf0ce02de7afebd030 GIT binary patch literal 535 zcmV+y0_gpTP)K}SIY zqM@NiLTrMyBwJtwc_*t#Hxy$>jx%v=XKb!CjqUOIJa=phcm#fpF=PIrYJh+jNfV%7 zHblS`@DMpxDnvjN_;8#n1tMb%l0JdPmTRT~WK<;J3Ao&GZ6S~la1Gq2oHGM@z91o> z4ZLTZ>je@51bhL9IqosTwq}?c03hiZxX5#peIQZ54RE^$AgnpIHK`y`Ko@xP0LTQ1 z0;<3__Vets=GfK@5&;3?6*%_{NQA@zEuak?Zy9;sl0?WFU{^`1C*b0vy5n{WpjXML zuLh$M8FGv?wFH)H8%|OOIKlpy;!o57>KVbV8erd!1#Is4JBr(c18nc-(Ss|1q#AHq zR`3P@=-~GoFvKpkGuQ?2we+ciU4SzW!F^-QEX$-CfTRl0Ei-rx0JMOEjB`iWrOp)` z2W08Lm*6-cP5(#OrJni-&H^YTcmjIcKX{|HEw4UYZob&(n^h{aV zD!&ln_vTtNz<(G91Zi}=vQ^peYF>HN0D51%M=~fOTgFiV--G2vOtxGYP6mg=c$4njB}9U&$NEu;bW8%pj=DD_}1I0k^&ji>S{L}r0YlqvuN z0E$FkYrr76e~!>^ue6W;oR0uYn@hG9^&XR7i5rD`04MiPioD40KY)s+Wu*S&FX0}5 zUYzIh{zu`}!Qmc22$qQK=TC#C=@$IPM&mHeIvW7EN#hmEGg71?ZxZPVe0lW0~ z-tM)B5sm<6+>{W?$ioqUA|<^J8~S^%MiY(zP$(^{I_mKC_zhonX2v_siiQ9H002ov JPDHLkV1nI8ONsyh literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_activity.png b/conversations/src/main/res/drawable-mdpi/ic_activity.png new file mode 100644 index 0000000000000000000000000000000000000000..c8727f572d0fb46ee0a4a8480a69d91586d25d2e GIT binary patch literal 1854 zcmV-E2f_G>P)f6x2Q%+6&mv%9dnvsV|{<+2*AHxRH2 zk%*E?BQ^nICB6*fxQ5- zF5TJP*_~mo?|J%RE_(s#Qfc}mPtJMIocH|y&-I-ns>=UUT-UKzjtxBf;OOwkCWqug zx64&&yRA|+nm*RMv&Uq(V0$>=En)ALyEQ!HL;sTp_UsY6rRk2RxfL5Q@rq8bkG0F2cb>U!0zcX(Kb%#|Hp!cO9b$=_*2+x;+;zm_k<0N! z;_(ODezAAP1b+X)`a}QX&6P&e*NZ)Iu-G(~K1{i<4aa1h$Z&ki>P0($bp_e!3;RBJ z^3}SUmX$XJzxrQ;1E@OmNA@!iJ&eop*rh@L&kuKhZ^bnUy!rm7ja9QNHanW)QtbUN zL?ADN(RMDyj$kC}=^1|GwO5Xd`*R7D6c_)@Z3kpD-bFOke?tfWd^&!JQ$w$ka(7o< zIL-E}5_r9R!?WeVit2dg66eSMbi=^&lFz5=2DZLb^GX9DavQ#F&xj{?Snst3ohF~&xX z3oXoZqY8~>4^v;h+_V4aw;RZ@!0n;-YM}FGZRRVj@3FjL7p0#0Q^4=6`vH%(y~py# zo%mh#sJc;OW2v8en_l9P`G>f>eg}d-N5VpljpL-Tq=>=6^NR{0Kq#3GZc7nM>tDic z`w5jSrQBZ3Z%^KW0%hL0ENOfWfV#4!Sjjrx`E(NiO_i&-HS`n!^}%ICM=d@c+lGo@ ziNR3?wMeI=w%`H3t07WTybPbKs^IQaCdu0;Uchm(<%-AA{QcwIs2V`& z>OMg{nzgV${4SsL{S5%eWccW#56NV*9&n6>8pWtXBoaGS@W7m^Rhhby20V6<{?P*% zlLhKZKF^(=FL`RXEtUnKuq^Lz5+#q5&G`yHTU0jl=Ic)-v{18Oh7w zn>9oY7RF>N!xArPC&}0)Z%2VZ*C!oswJfN4-0P~saln|YszRtWfiXQ)=AQGv6?uvY zwWfh#P%p*-a+&T6Som6_h!{lNs4<0(V+F5+9(@CjX1U%KK3h1d69w0F&vEnFcLpYUHM{)d~(}Wb6~oN zNbvcWzqaeItr{S56MkypJ>^B`(2A32-VN32^QD8!qGm& z%3GdIz=3dg!kw5ia^c+Ik)G~zNA6fM_v?*wD_aEIpn#G{4^bYN~JSB)jk%@$XpCY?6X$WXd(us?O?)X~eG@XubIaO;n2n@i{4 z_n7~grnbyOp=RpinJ^X1#pfwm4>Ke%$s-2n%;}8eV(-`~;As)@-}8`r-SRaZTkgB3 sX<^^la3^1$!R7RSAtF)=jH{~t8z`Z=zh($ literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_indicator.png b/conversations/src/main/res/drawable-mdpi/ic_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..bb4fee1053e0261d288dee8cb04ab82105341688 GIT binary patch literal 490 zcmVK~yM_bx<*D!cY+YB88U5P@0lj2o46Z6k;AKc6e*CgP?RN zICt<5bnB0Bv~#BxM_pXHbTFj|P7Z>DQK1S6ja~X$qOViJ6WW6tzWctr`|c#pIm9TJ z%cEkknDIPsGaiqhyWQ@k$j3xbsZ`$5>9o;qw^N-?=f*V6i(D>u{cnS=>(+ce9}NbB ziQ_mA)9KVtrBZjIMhYzvLXJzN(!1~bKd$Re0U()78mrani!pZCYPHq?umb?5X`WZB z)mKf^Bwg3{0Az%rO0U=RBuP>^=bIP+7>4nvX_^!&`u+aIw(XA)Q%VmgrT5VdilQ8e zJ{OCH!x;M%Yw>veEV2Y(Et0CLp31VUU^_O^hS_ZPA~KZH6GDg_;vfjHTrQhiU&`n6 zPlS*w%d&O>!1Fxr`~I3S77T~Orfu7|007Q;l!-(_ZZsOaUax;=vspvr!rTUcRIAn8 gLZPt#mzZ;oU-yr>(}!;civR!s07*qoM6N<$f`0eXzW@LL literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_launcher.png b/conversations/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..200daf4c951ab01ef7db40eea006578bd744e101 GIT binary patch literal 2726 zcmV;X3R(4uP)*gid&CGUC6&6Ty1MIm>V4k#t*TumB7B&O72P~{<-SqvXYMizbu9s{RkR>V zSufl(d}Ibdi3l&uer}(LoI;_mHNf>05pgrx+I7$Hkr@kMalyX7P%(O`b^&9&YmIN! zqO?U{d24q1z1o-V8Rlmez`3*4?YZGGjF@YUZ(Zfe;si7Q*+0si1d_R2J*_`1-~o*B zm|GlI5IS+K)@WSqM{}7z`U^Kw%x)&D%UHUEB7;&E6AOQ$=DvTmL$nmfwtYxMp{ptT zhaRF}4P&V?QB-bT@o^mw&jYPNXZ{x>#^tIBh=AkNDC!aozlLubc&3Ky*YHdYBOWz- z5kFL@A|R2mg6p=PPjaO`M#G!?{}K=Z-v!TuN$)u-xp!H#C$YT+AU!#X1W$|?yw{W8 z&T!%53>9x?w)Q$ROV6V=n1wzN0po*f$1v;eCF<^_Cdf44X(g8##CB{7xqdd6@1l?y z=Is1q2qu+CTHCr4S6_Y~T3L7wI0^6~aW$s%cY9m1&MuE}^5So()~689qQ7u}{X^eC zkzMoM4H2m1cCoAf%k1ub5Z}+F0&c(WMy9>i>?@3&f0PT0CvaUuIlGI!gI_~Ltv<$j z30S(w&b|lGQpTbOO<_&&Ewg2UimXog8Q*PTc-nDR}3?9|B-mA(M^CMZnuj2Z5xMY%1S|7>lLGB-8UR zrh;A#{6@DLL4QFha9vQDAo-=1b_pecix4aetSxx1%S`nZkWSFuz;DzF?K2zAw&#MC zVuhp=TM?V~5LM2OV!JEY_A-~3Pj--850SNi$9+@>5fKnK$k<^^>bq8mA7c_P3Y=T` zJHGP*%9J{+v)Xn2z$4Hx>JFWU5lE|M_r!w)fh6$g-WYHk2N7pO;MbOqN`UV4$|MO9 zP^Lh+G{}m5J_#QaTEv1dR<}tgN;K_2Hv+!>w^ABVt?gapqZI5Iyo=Gn+YxQ^#`_N^ zKB|Z$75I2c){ID*wyOvrS+@4whNVlb??m?k`RouQy@x^RU?`;#A}s`UsJx%v(k?_P z3<^o9mM)`E0a1zDmFU<4X+GflomLTnp~?+>^Ui-#%5B0oE)$oZt zzr_QGAHmXj8tx)*PW_sv-ueMXyyjGY<(5Y|H2QS_sGxp9ff3vaaad3WMfX3x8rNG1Zl?*Iy8 zd|Xp&PJoE&9)(0;NB@0{RBpkx9bP%}Fd)&Ii!+y>a>+pbJ5=c2W0)$dZ z5Q|8}7HA8Z3-t*a?gBYA$mX6yEUZj*gwkvt`bXlnc!v{&OQkfWilSInpaRn?i3SfK zY7LcsMP(rI9pdLTXMx4WG@|XM1tJnrX-1*eXk%!&^W^e_6muh~(MS@2lQIm3(SgGO zD1{20F4A@YJ_gZk8$304jEgJBf(4xl$xA5caKc-nP|{he^>d(vQg$fSnWN1}d%H1! zWSa3hiYUQ)C$5o0izMz%Yk#eStR0LOUR4-{l#U6ch0m$^m#8_j@NP*i zkfCnQv>1gEg$W~vm~=8E5)~3!OhD=Qc?*d|>99alJ>O$?=_E=EJ%w%LGd(R!jx_MW zAUe$Cu?6DTP7FRV<0J?*Er%kbBfxDWXb+=ZcK;J|N9ikG$M}WkX}C)revl;IoCtJ~ z`c;(rgES;6jzCQ*w3VdBl?ohIh0#YPQ*@$qIB{$Bd42Mi!N+xQAhp_pP6Tu&)_C-{ z5(ynS;1m*B?e8ej_e`)WYdUVqo2iK`t!6Cnz1Fr=5=LX9*$P9ZlE4BYpd(UU2q+cU zY@ocG?ZbyK)ES>WLVNsgA+Xv3QnFgP@KDRN0&lrj5&{XFHI`GvC?c1(7l;$pP-QQ} z<(p{uv*CB71$ydYZmntrGT9*0_LX*U{pL@jl+}DD1RD(@h&~cJT<|QE7M;%BTrM}m zE?lgh!iYyc+e(zEw_&13SM!Ik%OOzMELF=a`v4$;`sr_`Wp#Kw1Wj zEm6u3uyg2EdP-Z!=nM-h)115fGM=f2;!-TtXRw_m%B2A|^?roOx!Hu_q?JH-Cr+3-hbZTV81C7}-p%*2V$U8rC=6k5tr>NGaQNW5l z$3SH#`D|~8zjQrX+z657#vCUm{zyjW80o!%O+EW5=K3*WuyP6U=pgthrC4z;acbrW zS60qojE7QSS$ReWZe^&li`kVov9iGt{_Nr@whbO4XZ29Z4O4e#J9u5)rE{%F?kXoL@oszY+kW2i4(&ckrD|j($v>=_0_*En}3h zqTePKfC(~L)4aKCPFdPgT4{xbOeR_f(ac5h>2oMJ4I)MYi%IY)9S#Jzb*e8tVLviE gDaTs~d|1nW0buW&*|?uAE&u=k07*qoM6N<$f`)qvApigX literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_notification.png b/conversations/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..5d1aca1037f82c232c4cae28fe4f6c41d397d8be GIT binary patch literal 681 zcmV;a0#^NrP)JJXw zdp_>D=XcI|-x`&9q5;?pOaLF}8-zNS)T(#X*XlRRcv2x|n$%Xv{~MSDo&#M#1{jR_ z90OW_HejsU3g#nc)M537x+iXrJ2$KM)w(K^G^x+jA$3P+O9aK#0at+9h?;6J2%G}O zf#b7L)Tw?^cP)Ufe=47P*rt9~uPs7PrpXjV1>hENy-I2-qNa*Ps|M$QUqC7`i^B%s zB(N#aH2^1o#z0pGGy}V1@h!knVCFr@=kwjF#bWViZ*Om-N}*7=sah(Piak9&hgFKj zVq3J`4h#&O4ZhK>-VJ-u4xFA!-`Cf7JD1C4a=BctwY7CUK!1P#y=*r7eq>~1IGIeY z14yUS4>Os}yV23n;Z!PBA5ySGR@<66&DZzEpcq0@A?#S*Gg05$-`fkdGR)P$Ve-^t8inVv#<65qJgE z#1q7E73c&u&f&9y@+sOB=!T;AeZZU-PE!4(j;W8;^D+Ao^@BPY`8-k&TE1qp>ZP#B zGvNZT6X>9PKOBhJUjh$+L%?NVCGb7+eGBZZRKY@|Lw%|)*=Y)}1{nVXx8HX#hW$Nf P00000NkvXXu0mjf{?sW= literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/ic_profile.png b/conversations/src/main/res/drawable-mdpi/ic_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..0d056c7ccb28ea462a8999b54eabfecd81579183 GIT binary patch literal 622 zcmV-!0+IcRP)_tBd!q8CGmg&)i=)(J3Onb z6E8XrD>in`4$ms;hpk281eNIvwY`p?*|%e2f$+4#OPvShwb#yq2MM2n`9AoNYOGdA zh-)Ne0auD<-*wy=dx%n8gRh9_bATisxjpw&Bv4vOqj1b0Y4h?n3?L6}O`W*?HpT67%rSHA2Y(wF;O1lJnE(I)07*qo IM6N<$g7^{>=-6~$u*PAB|qpZJhlS&=vBWRRN7tvRP>?wBdE|NfuA!%hAhOV(@= zY}dTb=jO&XEnj@Un$7HQIh?|JNB_wEaayzM4fo6l$)Ee?{h#zN=hkLb9w69ZI^%^y2wz|5p9;);(<>&kwQQ$$3OVWD+4Cs1gMr;B5V#`&oi485EkC0HKh&v%qt zvdwgAN9)qua}!SqAGu~{ev)-hW9!@md4q#73QGiBI$BaL|KXoDL%ibh86NA+zyHtv zbKF+W+27FFNzXa-L$FNEWA!gjjx5z{HjcZ%{l86gT_1O8pu{5ix)$Acrvin{Tz~TK zU%357vaFry!t|OW-18US-*|+NyGL|$++)QrZ#!l44K(W2v)%P}UJ{(keU|U{gIMo( li%y!fTE?9h$*MWXSgox3s^_WCI-vU*JYD@<);T3K0RW2kZBqaM literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..09d42dc825e200ea22f36db829bba4373ffe318c GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^EI`b~!3HEJ|NhShq|`lK978ywlT#WR{s+vOD0qtT uVglzHCZ7L|3@T?it}ujo2{Wd&Gctrtkz;;&V6P-l3xlVtpUXO@geCwrS{b_l literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..20af01deafc19358befadd156b979db92945d2da GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^EI`b~!3HEJ|NhShq|`lK978ywlT#WR{s+vOD0s@S uk!#LDtpgwAIUKYk8l@bxGL#$M7%)U0kz?Mj_<1Ez3xlVtpUXO@geCw`9U931 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..13a878bed6bfe7407ea56840518fc04cc9cc05cf GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^EI`b~!3HEJ|NhShq;x%9978ywlT#WR{s+vOD0rmu z|G!#g4~Kw&4RWg)4%aI%tW03ZVpzGLmtoUkhFKF;n7_>ADgbI^@O1TaS?83{1OP-i B9)kb? literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..ad2dbae95ffb85c48def73f87395fe42698832f3 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c0!3HFsSlX8YDFaUz#}JO_VDb9>W4@H=j_USDE>S6G7^>bP0l+XkKHXRvP literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png b/conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png new file mode 100644 index 0000000000000000000000000000000000000000..c493aa5a46324be03003a85c99371076cdb92c38 GIT binary patch literal 1122 zcmV-o1fBbdP)-COR>^x*-*14sv;duK6u$RG?zd_CWo-UZeQ#|ti{=T4XPHTpp(~;JJRz~Yb7>0)r1po=YqAjKsY5qx0Yezujg9d;E zXPVwX<2Lj}AUq5b0GRul)}-ZC1+7~Gq8tnWBs}x))Zvw#gD`R^{FJt>WB!;6C0$y; z$l+bz9GJa~mawezxd0fLK|q$eaxrCfVER=$(tEK9Pr5W#2!2D`N~T%*9ZOaBcr@-f zSVv4?mL3sg6Ip5nPY-miXzc=Y*%lAgnKjeECvK)E$XK!PZq&fjBk>-$0)Q@HIeek? zA40lI|C9*FGlPoOx_pgl{9Q+*d4;jAiECZg&o{Of5C=UaQqIJnOpZ{I8Urv7`b(yz z^}+^#sISr^3plMc+7v<)p{4D!6&M4M8#40iM1toQ04S9sD}*w{0oH(gRaI9=g=M8) z$TZWHjt|eo^hfVKv36MzEK|r}K38BqM(dTOR!6NyG52VLA&uqs~XFQD1y8lJYOVVJe-S~WwG!3Lk- zO$4*d&<~Utf2}IlJiug4wZEwpW7iA-g+6Epie-5EltGDF&KD!y3ak*8`=!dylY zq+aOUM%V0NIm& zJRNmg474fEEhGil-@zz^N-C~b2vNR{ zr=2p3DSXd_O3d#HSi%bKV{;FSD;du)miPR_lQIh+h_&tO7d(?dd)Lr}Zqc$sz~jPC zTbAZb5ouFX6bcVL+m5SXYI{X+<0Gt@B#LKES^|G<6vWc#Z4&^troV%)b~?MS0I|00 zSii_X<^a3~W)oehZ-K!pxI&I7elQer4xfL6=Jt{^vYJCh(yvXSL(SBgD3AKNOapKa zo(W{xpak=~F03kaAiSwIN+G4r4P#AWGV$#kfR<^~Hm!X(t@b(r;0(fxbjMTWSpcvM zLar_0zgb14ZqO=+M;5}*&MZ7HaMqHaFVQWgy4Gnk6HJ1B(A?w;kP`rqqx&>AU!4?y zpno4tv_5{g1mG&@AGK1-*f97NJp^EK5&%$v-3A|wM!|(sI21-vQ~^5p44>9+s_0C{c}tVNa^ng9R*07*qoM6N<$f^5b3uK)l5 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png b/conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png new file mode 100644 index 0000000000000000000000000000000000000000..4e8de1b6174932146a6ed7dbdbf366c059f0682e GIT binary patch literal 798 zcmV+(1L6FMP)jLYnwJtvu0igI29&1>c=N3c68xV2%&;)?u zkMMZGQ;)$J)+-QEe>eg_d1K;x%gke8uEQn;37UcONlDO=${VGB2HbaFT!4TLB>`U| z_pL`z-XKS3%GUzW>HQi}rW&l^D`yK>jV#jxr4_^wupp4@i6sGO{uzN>3$7NBQ3-}* zvm+o^bjY~+fKP=`0E~AX`%PtL zvxq%MSX)Q%Z+%Y8Dz5C`V4De^)W7!D|I`)M_nvZWt*}>8c8P)sz>|J!sN!~!r*CSD z!use3`*IjaI5gb*!jORG?5q6NxlRY_9-;1eZ}*Nvyypt0*?DZ)o1^0P-V^NEDPr{P z3BE_dorp7r&I)}+j-K94bCW3Fn*fUl(;-^N#Cx=sL#j-0$hp8HlJKOu8>L3^1K#;s zC4?U1r8pgOCqq}f3YXNw5W#>^pg^Z~_xx4hJB(9YS?TYY{agSQ-#W>QT`V2jvN=@x z9u4NHeS;2E-gIl9mH#xTwkpVwZj!O69fhyZ0m|>*IrTB|+Llm}5s*lE5)!nh*}o)q z)*JUaI4$~Xr~~#YhVVz88APk5G`YYueBZ}9yQ;yp8gBgE2N|Z@bmcIfexdkaMM?lO z}>VHfjQx{A^Hj70e^-nNEdT%j07*qoM6N<$g64W_NB{r; literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_chat.png b/conversations/src/main/res/drawable-xhdpi/ic_action_chat.png new file mode 100644 index 0000000000000000000000000000000000000000..8a9a431411f0b633baabdfc9d7375e98a0dd6628 GIT binary patch literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XZg{#lhE&{ob8RE$u<( zQ>J9oG;jX{;v4+#{GIctNt5rE0`&sH!8OnHp3FJlS9H%yb9%V=HAcHnIX^g9 z_wc^o+tB~eRy;uTUN+;^hqhu1xZba2D6Q~k)#0*#IraPfR9mUm8+9Mwl)h}f#l)6b zl{4|)t0$5VRXiB?%QQCsS+)BF>l?|&2g(0=5Nvp#iT_!=86cC_vUhLEcY{7ee&=_%o=+AcPP?&FYad*g4nB>3Q)r2k-q? zYciWSe!KbcVgn{Q7Oly;)|UVVv~GYsI)MbzJLd&T@O~hmWCC(4H(^#8*XsHNZ33$6 z6DWfiKozJv#gFz0ECCF_01UtY%HTTId->eo@W}u&00W9y0--d7Pcb z00013D4d29@BNw{`GK#!IOpow6F|uq^msku)shH!j2+*Z;nkUHI5}5Nsv{e#>K)mw zWRtU)n_#)>@#_cBs7qLY32z9U;|@$3;jm;{jsQ;j zp6rI~B688%utk!+Ns!?Ll3&qdHPOu25DA;93HY2;$~t%TUxm;jx4~Hw&`ubCYk`0V zvh4)PZ`rpE9l!!7Y=gx>1Bl9Kwu(=Ro%!Q1k&?5z%YNvC;tx_dEbp+-cN*UX0D)>h zam8c%$%8L*XMuqr3QKp#{VH+7>)R!ed)c?rsRAe_0kcqm5JDh8o&Zfn*jgl@Q6!*2 z3xEI+fFXz>h!y|=AYiFOu(L>j<`AsW0w4ecm?NN5jN9H2)ckZU=?g6Y0%TDt#9Ue? z;5mtV)2m`1a`Plcl-~Pr;qf~8{#hzj^$!|;@8SbB_Il;ZkNr>5;($tj-3}YOSP1|C h00000oPfOm0|4WXd=hHbFpB^H002ovPDHLkV1n%A=T`s# literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_edit.png b/conversations/src/main/res/drawable-xhdpi/ic_action_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..67e056fef01235120a1f3b94e0d6f548b741b258 GIT binary patch literal 994 zcmV<810DQ{P)SeFw*^{th5gwjC8QDoKX>P=I59ctb!zrq95i0nCE!_X|9xs!$vOj^H^7F-weNQ}S3t)LHA>g2C^L9qI=ZNq)gf;rd0GaPXfxIyf&j zMjJjK5J$ifz?V*T#1L>K@TG07h#YW4@W*S!JOOV#=dWs=YX<)%1o#ZTHUZRjKKKg$ zOL749=vbC^mED|P4ES0EV0oHXf|4o*K7e;7fc5B0R+7%y@bcBMJv}M-It1h;zA0Sz zR8MQv=LxMi@Jxh@I8yAu1Axc0UKnjC4zSefvCbHXTa`yCJ}rk0_bkuI!tr) zBqx&wzN!L3hqXOmkEjx9;L*NS*w?k@M*?tgIRi4Xb7lKls{kdxMA9VZ)C%Z3oO}1M zLXzC;DBw-mzoGe#YB_aDdo%@5yITf6qY#;keIcPn2oz0w7RuobZEr$=p1Hr~$b6>T zU*)-#DS$71O9Gv$YcWtjn)mw3wSW)G-n4G)0<8JlTth%ZKtsS)+g||&0Hot+A@;cO QtN;K207*qoM6N<$f>|BBCjbBd literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png b/conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab436d8746d96fa4e003d3b516bfc3d4960df4c GIT binary patch literal 1179 zcmV;M1Z4Y(P)Yzs0ki6E&g_H zZtn8-_O=&yV#N~KFN*h5UOEUDUOzTD0AbK%0@O86LSf%30+eJ|6Gv9>2nbJUN7uKR zkPIL5fn58^nj8?y^oZqIaaZDbP2885z7zLl|CDXBN%=s|_2inq6$HqGKF9-K$?gNe zO)D|IDs403U37DD?kRq2D4}f)0VJU(ej(saNWi?(46sX30LT*W$bLOW2?&9oPyN@F zf&F}+asQiD-J!IhxY<^^!8$Yi4hxy?ZYoS~F;(KT}ngAp=0dP(NYD(Ng)(vU&0NGw1_>Fj{(`2yxDf`oY_MG}DUS>^FPoq6>LM=YRvH0^tQ1Cqy+yI2_6fi zz+7&RIQemJBmo)0t0a6-#`ODP$qVpKR`4SyASZaWhiDP3&pAJbvd?W5!@&pEM9gy^ zVX%)kKa3ksSO8vYX2%!>j0ikzc@!_JVhQ+>S;Z3Y_bi5nZWFeE*IDE!gaDht=MDh@ zzhN`@+@nDmd38B%hdn5h6jp%G6#^(vqe@V!qGbd4%mgSixTwl*co?&|KI`^x8HcCO z3O-K`XcC~yx7`Y6XQ+x@8^C8GfNwaeDr+f2S19AVGKryM2hR;Ys{)o|(#hLC05?MxkgT)Hd-uS2qK;?H%;Ma}?Hl-o| zhDP%9ndH1Yixj3RU@fLC(-+#-5WvNVg$SAd3WmRq<(8xXwd)JM-13|;vLpgrj^?aM tYC|)WzDZwNmsVP7rIl8i-S$_20RXOO5G<352)Y0O002ovPDHLkV1kOuAU*&9 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_group.png b/conversations/src/main/res/drawable-xhdpi/ic_action_group.png new file mode 100644 index 0000000000000000000000000000000000000000..fa2af49744b978c535cc59a972fc77b0a16afe4f GIT binary patch literal 1048 zcmV+z1n2vSP)ME71D7)4D)ZKb5 z@nGG#JQx5B00sa9fC0b&U;r=x7yt|a_bH$HnDl%;PuOF~#uz`H*hp>LKHe+U<(0%nO;IT1kLGK8<`$++YzQZ0f zVH?Y~tu0k+tk5sbyq6WlFKn(9yUU`$o`}5@HwFNk%$J~_unA`UpAM_o*mww z)J_4=$&5`sF=Z=IP?0MT{e8mW#jBtu)NEq-^MqHT!Qduj&!)hh4I*GG>mXIB@B+MB zWPW3gj%E~??W>k#fge#h+B6!d04~yyhdpy2)Xt&(c?R1VT_z~#MR5G2b9a6!d#K?ouBj0SW*L&YLBfD z`X0C9dmzkHUx%yw3DA!UQpXb7))VxNq07zb@GhZ@$Xamk$n2qcewL+zT@r#TjqZ+$ zRmc^ku*M+?ff;6fy-*LTN{sYNRd)ejv$cK9LX;e!vjWOtMW&b_-)Efjr0Y?$h4z&q zfZImu$QG-Ak`z-XCnEk9FokbDjia$=@uVmZfR$DK5+lZ{wR(vm01?!P(mk_rDFfh8 zQ7ILfb&i2k^x{-cb{vUOVt>f@ISLsS)i)7f#nUR0+~ z#s4d=Y^Zj}i2%>K9`QN_0J-m85ddykd)!6@ zT;qjFtu(0nFR`Qb@2v1z_a SQg==O0000~)xp|PcL4n61(4g;$56@AbHzE3QUk+_FtgUDK zp!zi+YO;!F=<7&!_ahatOTI_)Ce$*l>#3Hx>JBsk12iP%PG5BXTiKs4zn|sZ&S7Er w!T#h);t>uEg+PG`7iU&{%bBF&*?E_Vhhc@a;7?}nD~uqOp00i_>zopr0NlMvD*ylh literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..41cbab203c21ff1c4dab07b2a4848800576b0b03 GIT binary patch literal 753 zcmVe`Px5>F7(Ndy5u&*bRxAVvB)_+>4Dw^c{>CB_tVy2QZR8tSFx54ak1hZ=@=*og zNIu#k*d%$KD1kDMeFli;0Zo(FF#=|Z8p#QCn2^sfTXdI<2V9uE#uQJ6|1UsW$k!5} zP2__GXdC&UMG!hzw2i#C0C-+69Orp<4Q;)u|4j^w=rz%x3{&rclTX~l)SM%Saw zK{n<{Bu4K&0J(kIs)$XVrq7`L>JkFbPJUUp5~WJ$SVh_v7J*`o{G43hUpf?CJJyNK z5${OK?L$4lFQ9;_0R*4mk|r@V`9-Sa>j-d5^L7>y^y0U$%-bZ%`%Zy_1+GTuoGf`? z0ZO_fc>IaMhP;;mxYbZEm>@Cst%_pD}@$%9JYzzg7?VlL9{~mcg!4@;Ny{XR00000NkvXXu0mjf9uY{i literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png b/conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png new file mode 100644 index 0000000000000000000000000000000000000000..c0902a03e89e77b3974e24bea99e80bd9ef781c0 GIT binary patch literal 482 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+7+XAD978H@y_tQ`_ppITo9wBnj0p{T7g%>O+}X{rPx%gk~5{onq7dUK=Cs#y3s5715s@CjSK zasP&Ouenz~3){DP(k9KWr0@ES9Uqs5Xlr&)E0bR7#rRR|ak;2q&wS6)>uEL$tcBKP zZgYBa;x5 zEN|nx%_kXVznsrJ{mO^tJ7=f2)lCn&D>1=2)^q>&m}d>4&6W34{(a7Sv;L=?;bhhg zJT=Sux2-Z&{hm9|>X?t1Lx#?D+h5aVlb5kO94mY=@p^V^dhY7JjhAEDc|xYB7M@~| zfp|yZF!RK@Z=dp)mt1B@@Qyq$02JPIVF9z?%{f3_kNRz2xSgKN2=zNQ>Vl*5eV)C$ r=C41Pl>Pd`$4C6aFjYR`@=QDo**-xQk9VEU0f~CL`njxgN@xNAIbge9 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..cdc160d4c6210f179e79553093ebb2547f0330a5 GIT binary patch literal 901 zcmV;01A6?4P)RCwC#T0L*uFc4)8FYc<;-Dk=K$DH3;rA)DueC=? zXm8L8XnSb+cDvoT^n>sJsJ(mF_M9rl9h1gEE}$huS_XV}M&A>9AH~R^y#UFgg*;+1 zI13=+%Zi92pb+00oP01A0OWDTZ(0Uw3r@@nbeG-*pjp4InXwlbJ0c$w`AFgM zf@Z7=b|qO$Jvhc1KR@YJ046Xsfh3=+V8%SNYpfVoB`b67$P|_{y-~RCD>X5@2NE_lp6Lw}xphu;c=n z1^I<3d76;pxBw+0GiG^PTLe>Vk4!A#0_cZL9$*>+IMaX7iS&)AkMC{z*a(d@XDVW% zGiGi(A5&}H7{Dz}PrepbbOmVjeP3tal>pw7*Ak!>ssHo;vk-u?G%maI7S`FI?K{T<}=*z8~A{PJOq4@66w`lNW$0QwfuuD(_Cmjt0_1EckNy zq-M4olY`CL;cFYTmTDJU;h%=@WfrHpTuB~hKhx>Rg$oph* z$fAy&4H}&r&~lebHa!^h9(t(<(2vg)pId7Zuddi!3;6-;+O#C^LVynqS3#k63dCbF z^+X&#cKF)YOH00000NkvXXu0mjfAz_hF literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_remove.png b/conversations/src/main/res/drawable-xhdpi/ic_action_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..58e2e3b4d86d43293595bfbcdf7aa14d462d7dfd GIT binary patch literal 513 zcmV+c0{;DpP)2M=_pR@i* zPEjZcR^hV+AS76ZFCxHFf_3;}0xTpT!WR`#QUWS`aRDVHAj6vo$dP~!Zzf=k1ds5h z0`8IE6+U}6SYkz=AE7zA$Wy1p8|=~!7V&gK%#^meXz0%8wWZ964n9e$r4n+ z9g84Tf;flor_-F)OM-ZYj|(39WC)F4ccon= z%S0lPNF)-8M88uiMW>g`Wde^Gv^o6>KYu~1pq-(elv3sB0Kmi-@VJMzXz@CNI6~V4 zjMJb2z{GK-H_)bmT@=vX07Nx705JJAJeCcwp}FS`fY2gd0YWhd0OaRe!_Vy%Me{E0 zRfX^A*zkA-2txQ60)VZ<*Grl})zNi&gP`q(K{z<_bpcCl9s z`rTL%YIGCm_nZ>IUBGuXj+O6huF%JZVu#kWMR4sG0Ibr3<*#Lk+5ezd+6}apKBO)E(gmB7F;Y~X8n9j5|zKL!0da@1;E^~=ciKX zI6Sf01@ua#Jh$6@PqyYy60G#^@OpbtCa{zE^7-T8{=ad1r zH2})YOe=UyF(9z4+yZdq;sW+yrj!}R){~>S!Un+J&J1HFD+KhvlsI*X%nJZjZ$GWw zU>#}l6Gy83&MF-?24%*Adn#sho&3&`YJac;KoQwagAu|u#2v`&_l{(%&U#Rnq9_(e zUL0oyOlJ>Gr7Vi}PYwD0Csm600r&xe263dUu5#@0bB}XGQV20FM!Mb#nJI96#UgMg z$jMFcz2{iP(pXnx==)1*zHtC2{jLL^qdI);RJw`*A@=2{uHh{jOkBH2;o9f>grHIe z7)qFIi>TY2?XTQCMTd|SKu374tuZ=;yNs`99`Ef!kK~*Il%BAHAaDloocYxrgCHoz z0L&c0r~2d-6F6Idz9d-%Si*7)F3<%+J{kal5Xw;j@Ptrj9%Zz_0B8vT|GxG)6b%YM z1B9BQzt^>$$_6SU1I@|ji`4&5Boc{4B9TZW8k7DBFaWjb9TtHJeu)48002ovPDHLk FV1n6#bKn2~ literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_secure.png b/conversations/src/main/res/drawable-xhdpi/ic_action_secure.png new file mode 100644 index 0000000000000000000000000000000000000000..4e08b95adc68fcf00e8c4cb32668e4d50f62b442 GIT binary patch literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+7|T3e978H@y_s<^_mF`|o9EH1+$9WR35;wD?nE}|U8p`S zTiDK8l`q-A8lk{^fzit0S9FBT0mkJAk0{<(kDS(&{Au5wboG~Cf%Y*lSb1JMGGEL! z@d~dW-}A_6f9=*Vu>M_>x5n%fOZ}7u+d^-99Q&8ZqVZ$qu9dSmZH}fLTCTDtTKB-x zU%Pj|{(ms-uuWR#5wQ&`1pfp!vFtgz>)7&+gZ}GT@?Y0JkE%<&bK>mPc{~Lx|Az*B zmfrgHrP6b;lQW|=YO@cixM?0a)llg9VcR_W8tpx{Y4w|vZJy5*GCavu@$qgY>yKJf zRsQ5{jm2u(2ezLNzj$hF^{n=6Tk1~S8=+t}$W(L)!xJ9wXleU3GJYD@<);T3K0RV9%z)JuC literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png new file mode 100644 index 0000000000000000000000000000000000000000..bb999d85d4e128d988763034bd6735356693c388 GIT binary patch literal 1180 zcmV;N1Y`S&P)?qyPW_8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11Q|(0K~#90<(oTm8&w#FpKrAZHui>)Vaz35tb~CSDFRJs(u9IE zX-EMyX&mA>nSmw^X@dC!6q$)6hcKR@`82)QZYD&CszO=rBP?XRW~xOr1ytlZaQ=wbPNj&ZfRbDy;A)@8OqGbHfCus} z$N4Vju~Q);0N@|~1`cu_dj@5d4O!!%d}zHhPc%mWKuK;8@X_FB;wgvJxUD;udhG9ymRM zUh0fMM4EuBz$KqebWz8nh+Y9@IgfDNNAV-5<3U8PfZt?+=iGN$G$(Z&is%&pP?5X9 zl8b@^BkZA6ZHq`3@JOx^bJ1oYYgiVME?`4GMfuuB0cThekuIPluM_aYFemngu!m6N zTiqJy@KgZ+8*&fjF3y5yCMyO{CBz!kvITY|0!;#C~gJNlR8D$lMmJjjEOipP&2F z5fNSx&>%qDZi1i);%FKv`zZVFM!*Akh1Xc*dD~x=wza}6ssW2A1vgEk5VJ*rDmV2R zqqIciyONVz!8>aYy&kp3dIW zW6q&maMqY>ge_iX2RCF3XiyxZUA%w#3~tC4AQY`N<%68T4e0_N%PHW5jlP^SU~oga zfJ4p<)szpEZH`f8Ww79ebOAzK z0S6SlelwVbvJKR@rFEBOdIba{-u^rVH}nb^)dJ`_xS>l0v1gBub7fAKzn#Y
OwbU-4&*8V>s!3~LkVHdy&3vOr%SeJk|SsrNqPiSyMQ^0B7p~3N7M=HB0 zHP-Zri^hD>6!0>~2cRhlZZK=W(N}<`EV!X5pulqG0yw3?4IyP&O8uWfVVB1A1~-HR uUEAM`1UH0eYyQtza6<^h#FS?(xZyvATHvM7m0EuQ0000?qyPW_8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11sh33K~#90<(oTm8&w#F-}mfFwuz4?WD>`YF7fIDDLg}<2~COw zW=NBU6hM=P3?V6?NdrybUr_K5_yUoIYZLrKH!Z25|Tz2zPd zkrk^{^`xu_Zl5nsn-m`Gf8wNwpopN6D?S8xD&z$#o>JB65fwoZ!FINE+Y<8!yl^B% zP(-kiFI@FR-8|%gkrF`>K|Nc}lJZTD14lxH0)U_EOAQKM^fFM$LQ~{!Jzu=HnJbmz zX)k^#A^?ERT=|ZIywmSQLRl4IHM_C26j^r(&=OG37C$2S$x$a9HW4iWkLE4~#x(Cm zHSw^BQ0~s}YqJ}qG^3iB!axCEHS)##e-H3EqO}45pph@$hj1>YO0|@*a&Ev@M4N!E zT^htT=820Gs$dKIG;ajAlPLa15)#oVVCKD3 zCiV1ToJD(rtfjkZ6CCWmP5wVEw z0y5d(v6;(Fr1r)%)6`9aDdQQfEHxE11sbFlu^Ql802oDvh*(dH zRQ8MLS-@fWh4ely6egd&P+IxaG)-+Sr|c_HFNN7x!jOukiSD$Ch)qB*)Pi7qe8<-+ zt@NJR)5@bZ7E_AUR$huCp%Ba#Ew!;mzkXa>v6`f2vd8s-><{h;74SGWf95Hk-+)ky zf`(87GLcBP5n{mw&zS0LV`Z(|^PzkC6T5Q>ikCqE0(Ubs5L|#U)oe&`N2mZQ31pzQ z@x$SQhrQ$b2^C<;fJ5}A_Zr;MRlwui{22mM(X9T4q5{COfz5v+uyJRceDg3sz8o#)o0j( zJA456kyLjw#kqbKz_12)_yBMxQ@TJ(x|e|>ST>$l&9D8r5>IQ%-~)hCCXhq^ZRDr~ zclZEcttuR}{6{Ug!v_FT=EC;>Q4Q|!AI+4ek?Q=PK?;^V&#O+4XmE#*lnMabdf=o4 scld^;0O5%X?(hLxUwJ>BwBU~a0PdOErcF6>-~a#s07*qoM6N<$g8wFw7ytkO literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png new file mode 100644 index 0000000000000000000000000000000000000000..6da9ff7bd40abf3ffa2437cc91fa4ae1724dc695 GIT binary patch literal 968 zcmV;(12_DMP)?qyPW_8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H114T(hK~#90<(s>1)Ib!5zmph3$VCC6NL;m{0E!eq15FwzNRx&X zK$8X}1O+r{pb794JWF^5o*-8zPmu~!%${V|J9g~dnaQmENtU9lJ#+pqGvoDSSAaLb z-z>}aX(0(9z()}|0RFU1gaFTg^`76gN`wH5z^3K3R)~-+%Mg(*V5Z=lMvG7Z8^H28 z=QdJ=3RnTwDqhnl5h~yg@TKOpjS!&%CV-#7)DUePVnpn@EiDX^WtsUw~ zM6ZCCz$*uBj+KaB0ZYIqhi#9Ah+Y8+@C)$}y z3(tL~L?j|F;34onP~RyLh{y}L0{j3b0@jdX7R%j$eV)=M06;{(0=F-qmXr$+VH2^9%~o<0|GqPqA=} zxoh-^s4bu>A~Ffg(l4b**d;GT*y$7DO+X#GkKuVKHd!;c8&$`^cF5;`D;Enuts>I0bCSEHLS-4huV&DR2xmSM=`Aag|V^=XDkOf-8jrMC3ZKN`I(n zpA~vy2Xh-dvb|U~0v-X6y_6J&(%?#&fF-x}1fx=LrAY#61y`&By7PbDMO6V9D!5WK z0`3oAPcU{dtNKvEl_CK#4}T}Ym2nc_G`KP#Kt$$%8!kH;5ih|N+X#r|ufK~~+)|&N z#3&$0a3v-IL4zxO0=m!TH$xV{1qD~~BcMaLZLB@a;+6*L&q`iEh~Xbaa3!RGsDdk} z0=kcEXwpCv@);-+V$0(h%6tQ>;R8_P#J2nrhI9-S6Qk(%p|DrBtdnEO zd#4xwDvhMw=lTD%yEk${I%V(Iv#@ zdhH%jYa&uQE-Qi)k0*wej|UBp91{@~5kzy*Lqbj^KBLb?P(&~}7v2zH>cD6AsfZl` zla~eYwL^{QGZA(RTp;ATmPYo82#N^!h$^};@d$#!Rwoc}T_~m1nbp-a8cZ9C2ml}| zMz<)EZ+D_z6;Tr~`OCy-;_>sNPS|TAY61##g}|VC&(*}eB0||4=l(kPr;t@w6B80B z!R=@^x*rz9(AB)IsjUD2h-Rbvq>QCeNQ4`rWU<>;M4f=J7+zI^D`^(&HW76K3iE|Y zW2!S*6z&!gbpnEg;KV@e|0E*nfH6)H;f67$RK8PQ$*SgF>I48lem4I-XwGNdNT);6@h34$4@a3eD+Js$VoK$2%ggPIPZR(ElXKCJ0y%S}?lOenX0;6wO$FrV@?Qhy zV<+8ZNUI{63W(;SPYLgs12Yg;-}GYS`ge+VoFO* zL^Ku9jFD#}CkOu?)Kt{)aGIb-3{4sh4;nGGAv7uPKtLxc96*DYin6$0M9TsWUGN#b z@qfmLlv0|YCaGa?8i+NxyP^PSLUO4zk<`~d5vd7igol!ib9)g4*XhTAI) z9yE=d0^O`CDDuzC?FOUuuJ!UE%>dY1-B#$=*9!kb#P0AfWmx1 zL(KawiyVctf?FC_fT;v~em2lCsIa0cxzf_zrHwS}Gzf^{b#(t{72J|^26W{8KkML@ zIsw5#a3Uw(xh%iO=~Qq_oq&PLz?i5wllTNym7iN&y4jiFmijHgIQ8Y zZrO7Ns3Tqf_gZj^4`BP}^5>(mb_DclaElKB;rZ|bzznoBlB&!KOE2!dSWBg|jPLNa3lI8xMlkcU_AcT)!%2qEj|Dk;aJlPpgs+5@xv$Ku=3IPpFyfJ6-zI^>CfO6 zUk$1NvDv-nm;|@@h%pY}*af%vgfZ~EC&w+g<$s?D*#5u^1oQv^002ovPDHLkV1j+> BaLE7w literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_activity.png b/conversations/src/main/res/drawable-xhdpi/ic_activity.png new file mode 100644 index 0000000000000000000000000000000000000000..95ffbecf99eb91f75ffe1082231a228f1a36f2f6 GIT binary patch literal 4349 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 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_launcher.png b/conversations/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..927a2d2a565590bf0545375c86fc6b3e6c59f507 GIT binary patch literal 6503 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 N002ovPDHLkV1o8MkUjta literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_notification.png b/conversations/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..dfa643d058aee5218e2556bc65edbe98977e1a3c GIT binary patch literal 1407 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 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png b/conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4c9a32e377fc4859013dc6bd8230f720b933c7 GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYYs>cdx@v7EBif85fM4f-QhPQ85kJVJzX3_JdVGeYUsyoDBwE(x`M#O zj7t}mcps14!M>9@L`Kowb3RvL*O#15rACh%Ycl^W>U>%;*SB?wes5a&zccbTUw^dv zRNydSKXaaFJoDCvOw0wU{~2@|^*LE>^^e}Xv)GV5s&kuz^5%^{13u1lG5citL2b2i zURloucCCcOZBImJ%$U=eB)dPLS3k3WwN^v&z6X3Yxy|_gfY!ma zN8T1Og&yo$75qwH=((M_u-L-%vPry5A8H*mR5Oz2?-Z?g`?S05gmfK`%`DAhKA&%7 z*myo>t1z2YJz1h>>&5DGkM7u?z2csAM)vYTTLr22)tA3d+?^6$5EbJXd(qIA^Eumd zw*M;{yZ>c4KYZ-?My*o0p8wC6nA?^t4=!c>W4gK_p`iBDnO7_!${F{`|@&FVdQ&MBb@00ny; A761SM literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..5c2440e4a8c32686d9ae57d80b58eb21ab24eaa1 GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^Y(Ol;0U|59*B=E^rk*a2Arj%qDGd$(1Ekyxjha~Q z@2|h#p`)mw5#gW8+4kR|A%|W0Le>P?#X=tf8reCwbucj`{&8dGQPFh*8o=P`>gTe~ HDWM4f-qjwJ literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..e9ab742e8c44b5ff24c73383f3093cb3b159db57 GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJ#0V1dK=^Fznb59q?5RT~Nl!k`?0a9*;Moz4b zCPqK}Q#nl@EA}x)|N8&`e^pAPo7*wA87y7JjY2#>R5w%#Ft8MhGdEPQ^#F}v@O1Ta JS?83{1OTsZ9)17- literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..42a2191eef3295a7aa89d3de8b07bc62eada0adf GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJ#0V1dK=^Fzn6;Bt(5RT~Nl!k`?0a9*;Moz50 p7gWv#L^7r9F*LJYq0{)#k)gAVk9qdrkBmU|44$rjF6*2UngDMG7h3=T literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..a5a2c25efd72107bfbf82cf58e663faf6a43d8c8 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJ#0V1dK=^Fzn9Zwg>5RT~Nl!k`?0a9*;MoldD y_t)R=&{5RTi11J4WLqH1d*jW5Wh`d=OboBB)R@~&|1t&YWAJqKb6Mw<&;$TX-5M?c literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png new file mode 100644 index 0000000000000000000000000000000000000000..2b46dbb9aeca4b9b3d01e4f2b2b433a065444c46 GIT binary patch literal 1643 zcmV-x29)`UP)S&}7NhWg&~OaFi+)mMKi zl|l#tfj}S-2m}JrI-)*I^5f%UOn(gEVSg^@bNsb{D9Vul_W~c$ACL4&>62)Dg^|IC z*ik}`NPxZInXoeuye-c3*-#>ONPxcJnQNKTXX5a+jEH6EE-DH@_)Oau;NN{C8J?@3_OUyC(p& zZ`JXdClL7W(gJ24vmFyGLUBcLi`qYeO;!jPgff>03qdkJ`UE9{VMR%R<=&?f?GFhV zUCUO^is-*hp$w8p)>^>Y-d*mpBSQ^d5WU*W#Rdq>{og>`kBDzvn2;PSOO&c=m;Fk= zduYW4f!6;p5I)a1IMM)xPe9W-Mw-GW@VOQ2W&9EV!e3hmP^`fuBuu0g^EJt+mfi>e z*Ri(L-aZh(T@QhnRalac`Lcj{1J`kuOg2=Er6j^Cy|ztjsrT`u5R5V)|AF{n_Q zR?^y7GeEgoVCV##8oX!fnt+7d2V7a`(8dkLJ=R=TICn`VTEL~-8MQsQihxGl1rWsh z);2N>iGaf4V$J~zJ$GmUMKBg1xv>8OCg3PoY!eG8YCFB2}T2wdZg4MkFC`D`}VXbs?UA6<}M}MGY`-+b)TRNSRPvic~3C3TT6xQI} zfL99kwk?ryO*tW#NML#H8!uofCdUN7CYOgW8%405d)EH6MEl=`!^mTp;}0GL2V%nsGBdNVan#+;I%ILlzCSzBpd3u4w3e1Q2jO4lR9 z1@VLRi9(2H2yWZ5$$>jiwWc^BK_Pr*vCkFX(do`Uz}lQ74pD;zCgk!MkD231Cm7N1 z&WXBw+eo_<)~eN>2o^DxTxl+*t*#ImXg-_FL3aYMhy*5m(u^ze9^t!*HGopgD(YAT z2>)1Nm4%5`!&rl+2lJ6es+Ez?DjyF{hqB2ELQf^(mYS_*gDxL4=K;S&5Nv)^QeBO_ z3YNmYKn|)q80T2w))HwfzM0(GSVyGL-W92@h%$eF5>nK<0L>=45hn^HzYu@r?<;p^ za3|pi0v?e1QgZ?092ybOpbNsJ_VO$V#I)x~w=j0)!hrCF$8CpE5)mnC8$Q9TU}NaR zO$1-+5`gF*#Q$NFHVHBTE7I(OT%EgmgKc|V7Q%gcs;LY{ZG*zj*w>*Go6kQw8?;E$ zzO^U8Ti;p~Qo^zrlM~A_uwW4{o|KL$+R}u8wFiyZ*k%W9!muyY1Yhc`AeQocmx1<) z`FRa@Gx99l+V?|x{6yTJO=MaY;P|%U9%?OxMoEMFc;}a^g*N($5F_?=X`!| zJLQ`ppo7cUO;Y7~7r$vGLg8Pyr02Fth*0=;J6KnUJqSPRsqlGFU$uFMICd=j;C>Z= z1;(W2N-e7z(C0tlnT28jOL7~?>RNAtvjEsZj>+v{)++)?csJpjB5A`4Vu#v4B)5lI zUo2okZWD>`YHjR5`(2l^gA2%^Tfif^ZKRMo zO76t;9XS&4dmm%m7X)SM3j)g8?a>8c3_u_ONB{x}Kmrg*01|*e0+0X%5)c@$7-t8$ pfQRSe2m}IwKp+r^KH;YT0|0;J&PG&^m_Yyl002ovPDHLkV1mN<_?iF! literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png new file mode 100644 index 0000000000000000000000000000000000000000..e9a58eafc2870f380492f6bb1f1928a32bbbf9d3 GIT binary patch literal 1088 zcmV-G1i$-k7RCwC#T-|XRF%Z@v2w!5$#!ic{fdK!Lzjf?#%J8B^So3i1ZJ4r9vawY(;@SV*qbZ}8lSCD2(WEa~xU}v!c!iuw|ZLEH}`7-nV z2OnxjoeIEK@ZIw7KZRPZMPTw*#>YkP5%x(~i?A<0pI7}?m45^0U2SL=)zMXIM5zUd%FBKb3vGLDww;7Jp7i91vfW z7?T3BhNs{J;PCNL2`+xU!V6o&IS2tb^4Kc5eF3%;^iq-tgNEc2=^M|;S8S;(NiWp& zMr^C(_XOB5mGpB4g9fXA;fc>L!n~}9SpWac7-{bU@TR*^VPNf}1*}v!7_&9|Apo0T zs=~s&?*_g)7Lcj%Fj74?at|u5{yQbSRT?xXZ?aTCKLT*#>Hw;Qq#^(a5EPOaCaD97CO@NTp89Q9rbQ^d4jo3a&|IWY znnZ%7X%4s`>;VWvV?dLN2s2epm-==4&PsnIB*7ZAZrpFiN6R(}z2Jhd2S5M-0zd!& z0zd!&0zd!&0zd$uLkvNI6=nkO1WEt^00000fI#qDfB^t6wO<{j0;t{q0000A7Gke1PeeeCZ z`^^fsyICzGJ^Na}{PBg)j?7#uXY{Xp&#!t`iJw21RQmNDKH;Vg7@R#Wp3valQ*WC8 zLFW07$NP>M1Zk$)Z{2W@k%^;$foZ~$qp=s#y2|D6F>)v{_^xBTzW&8@@teuA$CI)c z^^$*TK8hL*eAbNo`jm4>%h#6dO1^^SJhb zY2DpRYPz2;UU(wZk!tU`|NS}lcK-T3_LrPXWPz42FdWEwGp%CszW4kupKkq-ZpeH1 z!`5%U-?{gm|MS7WmD^%@cFgZ(`jZZ96L4T)R9F!EZD;<3sNKm+_~%4DD;H#7n&s(u zv16K$t3$V7!uyQhA}Q}ZxPq90YFR2+#F7?@d+q$*=h7hJ!oZ=xz#_oF#NqH-`=NZ; z;`4k3dYP%UiVyzVSReT5bG==pWs|bP(utK{%GU%oa40x%t=h?5)v~Bx!}V>G+(AKA zO%L%U9Iu-cSX19L9eT^V^%`sIZRtr(E)9AdEFO+q%N*5`q|1TC1y;>N3JnZX?zZhK z{i&9eyvp!-7VDk)8@tN>d9(bPyK?v0oZ=6@2j1tt_>!u9F(3 zbuWRq^z53J#ecVNhPt`Lu{-GSy^Bv?GlK$*f$Oo>0fu?9F(>4X3aNqwJYD@<);T3K F0RTJkyr%#F literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1260a4c68d931cb7dbe80341355c1c2438bd9a GIT binary patch literal 765 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 QLjV8(07*qoM6N<$f`u#-@c;k- literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_group.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_group.png new file mode 100644 index 0000000000000000000000000000000000000000..9289b1c8f26cae59fc1166a1f2e71f54dd15ebc0 GIT binary patch literal 1475 zcmV;!1w8tRP)xN?kRog$ zKr#6dSw1I}C#)gBA^8i6^~b=o6k!1Yx*^{y5qchwKk0ycg)u$*>w17BU?_jA7&cs+ zjrm8HX=>LVFz=9jLC?Pbk|BuF&Cr7_D5NU86MS=2tzOAt5}HzbU`rdGSp-dmKTxUf zET@K5T1_ece$rz9qh~eXjhtUX^PAGz>V(HuRd%lh`6~-6 zR=>Kns zB@M4%vI;T_+FQB62KU@8Eo9Q7=$W6&aW_#XdVg`xL*5LeDU zAp8Q>dts!`E;e*$0`wg%%xCDN4_e%(&mn*-*Y^2kdE&_6gl__fQ}TeOMHROIuN|j& z?8sUX4(gQJT-h|^;+%)rG@>pHSNE+Li;ehiYbg3zrP#Rus_Q4MSSM~T|8CZBu^aL6 zF2KZAd^ER?fw^G>TWzfynxl<}uSkG|?FEnO;mh$)8&Sji42=8eW5CV43Wr6{Rq>zR zQQO=v((W^2+l1*%jqY9%wQlQ7a*>tkWFbXvde2P03WX-YTg_?}f#bR&Xszq_&|`b< zs7V%dXQXR$If-SF<&w3j-v3y9KJ&9K>8C`@p~p-y^{|&cTZ!;k)Vm@tUTUEs>(4v*_33yD#quzxKG+(SZfN}btjSB6z`=s1 zH7eatZ^R5C|CC(sOauxQ^PG8Io5WeX5mVPawK-`QI9OQGyHk~>zR0no7@L;V2dk1t zv^q*i^P7wOvq!U{x`eJ%Yf|sKFSW2$^{)H40ywhHNyX-?NcyQ2N;bvcqM)tdJj5ME z09w?9EMrF&y05rKdBzRJ&8^5evtL7m_js-H^Qvl^Qo*Sx?n)kzuYHR!BEcW41gC2v zjf;(Kr5{)jKOlm_g3>#t$~d7g)Tin&EU>_5vg*_j;kIV%jm2TlJ`{&XNX+Yg>Eu_0 zD1BY8T9dA9T`>u~cP0Synf;k=Y2+;nhE|1o#v8t=7k{5**DXeu; z&{-?OQ7E_-{+$&%sFKf4fQ~*#wN8+s002ovPDHLkV1f}1vAzHR literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_new.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_new.png new file mode 100644 index 0000000000000000000000000000000000000000..c42c2bfb58ae772ea54d1f1b7a74a94e2c8a2b2f GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X4tcsbhE&{obNe7?gMvsyVz1e%xpP*|%~IQ4!udtP@ymXL>W2A$!YSA2~)w76%4K0S5*a0S5fcdD8Rv zceE*+xnB=)Yj?TB;6H(ZLxCZ%&M`j0O9;PKB(w9LEqJYWi4o`^fwP(i7(N^q{1!7k Q^CifIp00i_>zopr0M9i`&Hw-a literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..ce7536cbdd755307df1806507498110477cd3d0d GIT binary patch literal 1048 zcmV+z1n2vSP)6BFc`(DAXg1w58wqn zK;66JyU}|*fP8N}Kzp|bsP9b%=m6CM8Xy`#1GxMgs$~gUrYK63$oBxkfI<=B?}_~Q zPht#$AP9mW2!bF8f*@!xC81U5a=9${&x$AdO~vJcr<_ufBoz$k4}8YcvLPG6KT{fu z5lJBMpZL$$7PpiJt97%)71SqE;yD-7rdK0W2{A`UDXirI4$e!&2D@TqVAE5pdNxKneo}fG?>)3-AY@G{0j=+y?XvU-!gUFu)Q% z<>%=KPpM(~G$ETQmvb0k318&DPO|QqAoA;G#Bb3X&;fp}MiUy!r5ms=^79_e6I!ozDEN9-=hJ5@6iCj_c#n46MWj5 zfm zM-u_xqXB^L(Ez~rXaL}QGyw2D8UXkn4QQzFEtqr=>L=j4n}}ta3YQIQD_g*KWB{-9 zlIc&DHEnP&Ow5W2ewP-oy;0#d>+MzS@U09GD~BuPa&EAmQmQRNw4`X^lO8SZS!u4B z9P6XAmuux3Ed`(I?#m~9QLbEOImBtX#Nr>NbCv4Z%Fia78vI^@Gc)a?hJ5mAhfIlU zCtluZj4?nBzD0{2rHq*!0}r~}aWxE;VVO0z+ixjG3E!0g?fSeAup4UP8Y3u{Y+`|L z8-!J*7A91}fRh+irMiF@I(*v}kZXpMX@?e3#R}infWmNSYOPq{yJ`W9>0kK){5}j2 z{kJ;nopEqmz#ncDr2E#wvj7PAeH$P?^`(2&|7c@EtnjwvAm0gKEJbS3N9Ad|0bymxB@MN0>uQmm77CD z_ugurGFA6=eZKg%n4FWnhL%jncz<1+*2{K}Rq$i->67J>#~vRz&GG+&pgoJ=hg4S) zjspIFe$Do3N{Y@7)gSo3P4Ef%d(+E8Az|KwYR!iyH*cAIXW>eh+evJHIO}rw@5}mW zZkh7S>RRf{UCy%a%haZD?BKMpT2q#iIWv<>Q`+T}&Mixw$khE4*z_H)|8#E`++Qxc zQR~s=M7Jfy?}cl2u;s`$%`fv5|8+CuyzHei0VXruOn0|`OFv)PH+S=Ot6y98)-cYh zn^jb5_QYxBbFJiA8~dv4Mewfk}XY!-3(>%;z(2Pq&_0`aQFV#iz&cnF|+4 zg+K$tjC4h#)Th6?&E+RN-&fi%_cSx|;-)BX`OpLJmE2Yu<}XoLhhz?lp}067W~}_b lY?g2mEMgcKIu@;FU}kuAu4M9W*_ibpaZgu2mvv4FO#o$q;A{W@ literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..cb847f3780ffe9c430bdd0e525ad2e48127b7b91 GIT binary patch literal 1274 zcmVFccPC7P6*ey@nC) zn9{LXVa*C#kF|e8t{bemZm_b3m5yZM&WvPDRFH@%(&t~i`}0))#%kwWkS60;UxvCJX9zDs zl(`>feupk3gqWHpGbuqn;b3PhfHggJ0ezuvNShzDAQl$n6RyfwQIG;);?I2gUX6`z zzjS-i?Xp1N_gVn+PP!mXJiYY9Gw~^??^AA#q1Pb!lM_2)dehJdIu z0?_0~On#&QzXD+Lzt?a%6umsBF@9cbuKO8s*m}s}=%D7K4LigzDll!In9KbfFGtfU%${)0U|EQW(&t1D3=q!k>URf$)&xW7631< zm?ED!=$5kpQ_{7RnoEHI?83B1P%R_?j($vYKF^?9&H_x@zm&*$A%Hi0G6?`QH){be zpxI9zkOxFR1#mGJR5$>4HT>j>$8^(H+ zwm-Bdq%B?Z`-3(MU@E<65r~Uw#s%#N`GeL9U`Jre_BRlKEKJ${3Ib?W0BZ<9E(tR& zHYIgtdm#WbjH`V}#N@P}*11g^&0+{Z4r7bYzybud|Eu9+i@K|Q5I{a?o26;1P3_GF z5Wsfrk1X6z;$owjk0QPIF?3X*SqG}P7~;Rw1!}heS8&`dXHU>zNKVt1SA$GaR*oO;B9FxBi()@|# zGu*oDI}`zkkYC}vqFdM27s|1fGXe6;p4>vmoW-tR%!dH#5_jX$=Vr=i$0fc-eqW@z z$AhJjqOKIa&I|hK$dV|h zSgK5EY3iOb889fR`CwMpc#!h>O$lJ5+J8FYzc^na z+G$zOV4a_ty!wgoq_KK8L>Hi2*8{Nfcz$5vB?zx00000 k000000001x#cu%y058#~Dpx%2*#H0l07*qoM6N<$f=F^#1poj5 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..331c545b8cb07a97ee63cb4f1256d1dba5557a82 GIT binary patch literal 681 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n7ln*978H@y_sX^mF+0u=KlZOu|s+fxN=%TuH_vu-63%O z0bh(`mh@(>)(|r>v-Oq66AmapP&R+I|NHM}^O&8P#J-=iwAdNbrpXGl5g7zooZfe! zXy=~Q^508s)?R;|&#w7(p8l)j$+D}f&aa<&y-Vlf4wmJ z{m;$!KDWm<)IWJYp?`yVy?a)Rgy+7R7WpU3-7WlQo&Q_^@0!|=9dkD{|8ah%BK}}y zUwJzR`>Fc-=T0VbW}WtqPP|eM_FyzT{_JE!R zD)mzsyaca2=@)ZQY+Cdukl9DlG4$UH#$_D~bm}KDB)bJnw~uTvRI)fR-^3w}Mf~Sv z_7fcsRN|EmXe#eGIi0hhi=*$;Ql>v@O{@PLixhm)QT}Dxr&MO1o`9IAr*$3O_1veg z`?Qpqrz1f--tK_DQpAbv90e{c(m#7YGyP$o^P)53LpC?loVb0lEiVH0g|-+>zh76d zzw5&@{nHH0@veKsKFU^vF}CfCD89*-Dfr;V_F3ly55?}QZh7Ijuejxf>%Q!k7oPi) zTVD9?i*9)lxG%WnMd&{7mKTxxoLgSR?yG8<(OmPC!%+O^D#0YbPom04?mSuT+F^Mr zylcW%?L%qnpM8_#v%0PH_*W!j7~dy$w}_iMj&Eb;mhJQVTeVv6|AD73EMLFabmp+t zH{}!WBYrC$Dt@-J@Me4I8>JZ0$#0vg1>-jHzhy>_Y6gY|m493Y3}5QcuZR|#@EIiM M>FVdQ&MBb@07pA8tN;K2 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_search.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_search.png new file mode 100644 index 0000000000000000000000000000000000000000..a10863887e448e59e0eb34d7d08eb5f273498628 GIT binary patch literal 1152 zcmV-`1b_R9P)No+02}}-E$v#0vapRMt1bOs<(e91z^0$N)!mjH zMGyo*5ClOG1VIo4K@bE%5M*63r;EAWZi)OcreA0BDC8mN_dJf{ZvZfBaAo8<89Qn4 z76s%=$CnR4Vh2W7BRrMIQXZ3zAE!W^B#0{jG&g+u5N1qCg{i|3LBu^7Uxk>Rfe~M) zi#+xcL;(Q*jlZE$-&DCgo@0B`!fVv~Hxhyf8UTeHW&A}0gQ<~wc@;wB=S^v{{-BAw zZnFL@KkpnQ05bluCbM~a0&Ov$?72g*1z`7L|<6>yvBE5F0kcj6zS9{zxP2ELYJ-4Fn<2{gWerk>R+%b7A~{Bk7*?tZBqQ zE0Nc?@)`hzovJ+lV%zu)A*7T^`3K-3E2*4tdyT1g2&alal@qp?9-vDHom8droEfFL z&sxx6@eaVUD$h5OYIe{`^ZLRo0F?PmS{R@&FSu&FVCkC&RJ8?iCdE&t$%v*t0hnnD zF84*wJp>jFuX(Tl60QKsxv1i`i5CE<`qaf=E)qU_KvhbYGfTBu-zBpFUMYDbutm zR*sB6)p%geZIPA{0MvIE8ZVeT_+Gk2TBnGaOcr7XoiTK6YfGblqe(rl9M_2&13<2Q z(S)L79}wg@mMI*b!-~~jr^@{j^essz$gG(1GLdp$0dZ+u;Eqf<5-jS zF8U0Et!!p76l`mXx6YgE2<9qkwtGemiZwS+u(?B~4nhecYqNlhQp`p4-Y!da5UlPI zqeWZ48jR3#|F6&#b!AVd>3-tDKKIKVHKHOAe+<3JwKgm5D=U^D}tJIifqoIfq)VgGOP0Auzv!pl2vf?5jK7N5@=0;|W>ScE*MM1Wn!G>XE|{LS}SZ=r+& zpv!7#s}Mo~&{ftTgaV-83Lz8#C$13UKmc?}N3CW@!2sY2At(TrApGdE89lfxsOEVR z@5~9E*1-muh8l)20RXoU1OT{$(0QS6@Re{!2pQ5)ciEAV$DpAX^qwb_|BFi4fhPzm zzzP897lJalTEk>?g*5=^A(TbdgyQ`bR)HW0f*=TjAP9mW2!bF8f(EL;0t^5du&;5r SwHQ+X0000|s!GPh5 zjO~TP3>OYFNjC5}uqiMbI81Qlm6<4-)Z97$yWjIe)1K6#qy@HMM9m&YwQ5T-#^cX`^1De>Ep}-JHl*Fh%lDq=@OyTTRLQtDf}-x<%gJ zc<%PS?+rJ8$s4uJTP^VA_Zg$r59X!uxVG2d81)#!}IyY2q010}!`njxgN@xNA?fUB0 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png new file mode 100644 index 0000000000000000000000000000000000000000..12ec4d33f19d18ef0bde48e635cdc671609bcee5 GIT binary patch literal 1426 zcmV;D1#S9?P)ELnGt z|G(SkR?_Nh##?*={LSY0F$r}BfB`ni%oE^25}}J3Z}ATB`NVxmgd%3V#TsxY=dmP0 z0W%(tAv5=Zm4e?%B0ORR+yt&w{B9EA0W07-aKrIAPZ1FZ9_#82R=^eDcFpG|5e;Ak zECIg(=jPZ(60rax03b8p0&gs^ts(Gd5>byy0UrP#H?YkiNrW?#0Lv5II@*Qp9Ni0Gas-c%_9}dMP4LzzyJfE7b^xNNd__u?98r1Y84d zwo=V$dLbfDzzT4`m1^?ow1`;&0GYW1tf^O1PA5ea3HS_nSG^iHj*ydeCqxtpcnkPS zy_$M-LPU{(7lFI#)y&5c(wZK%UC2D-3joN>FThKx)htGoBFY4O3B0XZO*2F#qD;W_ zg00v%0{&uiyxVpmWt0iH1pKU8jUSGY5fP+_VgUe|xd*JORpXZ+MN|p+8hBH!8XbZX zQ6=C#;8V3~bO}O4m4JB`Y{f>aBB}%|0lxz0mFj7eV`SE}qq);^0no&Pt=Lc%;UeID z;1i8v0^fDq;VR%PvT#>(!B%XHkk(X7gqr|>%zT3^++D6+TED0z z!d-wxWVhfJ)$lEXIrXr zQAWtg`r+iAVF;*$ObA>=trF&>;gpE@1h|qJgS57)_IHtX-t8R$4?;Xbn!+F>z}`&3 ztu6im{*7yvB1iQy`R!)@btk$+Y48?z0h##`SSQTB8WK*yTigU>=6O>1Rn!XJ z;yMC0NZ}W;jhtLh=6SNK0AH6ch?#|gwh; zaHh$fMuwN*EiMB3+J3wSZ%G1XQ3>8sEO8lw-gJI4?i~4D)CQH!CT5kz#83HgcW4)mNEgcx1U{P zR&&&LA!U>ai1h^g2sz?|4&Krw0Z|2S$rq5Bmw^{_OPQM}gSQlofEbScJ>-H-Q3h{` zQ9vhxx5Ol%Q^8wi1!QIwxTIf7A$v%VKWw`YALNaIP>%ji2X6^cKre!~gd|`e>G8b? z-f~)iERwyfTthE|w@i7tW#&XdNl_~j~_O;&CW3ycvI~s1z&?E1*G8TKm;td0P6th@lTs7s!!MvaGCnw z=}N&jfE7@u{iNUg~cNbpZQ*3&y&1f<|SG9WW+$VDxtNx>^HVD{)w!7DN;AO+t7 g17HknrQqA)KP^!_fB_5oO#lD@07*qoM6N<$f`W2mhX4Qo literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png new file mode 100644 index 0000000000000000000000000000000000000000..7719f81a9a42aeae09dcd7d226ec6c241b6d6162 GIT binary patch literal 1456 zcmV;h1yA~kP)KKRF@^wvqrdaiMsPgLj|_1vF5Q0w{nY z6i@&Kp@11kn1LoKpdegcf@i@z0&Q%^RuaXswuUDV#o6Q0WiMx?J-6NS&8Vy-30Ds zJQhS4pfj0FNTT=wu#xjSL4-!6fZM>0lHUy?G#~}s1a2ul=P5$OKx194K?=AA{8;h1 zK|~Em0c*f-z=b8Y5k#y&XEK=pkVNsC=>7{@VdEjjNp`rG6C-ZADb)3C{Bnd6R^yJE#I&zqD;UV@GEf9R6!OQA!bbn zCU;sW0BTvVMX)>w2Dv>a1Qu``WI~Zh87W} z0sxXI{tkGh&K-M9ts+zfm{_po8>fhq_5LawQ59fv!Ip2VBtliddBnn9lMA+dV~l7` zl|(2B07#Y4-A5oWp^Onk7l`-OE{c!qDCj?X>oe-#q>fz?3VMc^^0#r$&xPxeI zW$o`WcHZqB1A8tWA$8F~i~xD-lr^l-ngt1uJ1M4Nylil3uA&^EkY*hfr5$5{_n`oq%~VOarY1-#64Cz?cI@DgPK zNfdt%Y~yBMbqS^5B}xL4D1M$0ei@a5m#B__9YXkd>?2ODr^`H9Re-L`7x>Id!ApXG z>Kq_i%}T*bN(H3TNgH3>c8ywtm#B<@ZQxv;JM|1L!An#Gw6*hNuhxB zgOZm`6yg8}h*{0Z*z%~5+1WJ`d#1U^acuARnCbu7}!Ar^nG;uki6TwSz1*9K- z?3gP>J5B{JDH;KrG-DB#lfg@h1o+;54iU4O6WfIpQ6#|E6YwL%5$|;Hk|qi8DtJkz zfFz1v0J$)yUxF^IyZ~!}=kSp0000< KMNUMnLSTYAEOQ(H literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png new file mode 100644 index 0000000000000000000000000000000000000000..1889581322980550b9d312e62980aed751d33286 GIT binary patch literal 1433 zcmV;K1!nq*P)lIJ-h zau3+3xK9${5G~*~aHHYANrVHmfSbTA!^b>Ei8yjt*I>{Bt^q%^d~6b70WDw+_zk$Q z#5R(M6_7m7)ej7y-Yd zUY@W?hB`+aTHbCiI1>OMB6oo6j`o$2L@Y2b;6317$NS7kBF>l>@G|g~r+sH65eot? zpq>w`d92|H>SR4g5i<)C=~!8B0mAo`7LK9q7+dl;8WmrzvWmVDiL)8<`-#f32HTx$Wt^2 zI)vv7yQpdCr0m94e|YgEtSrE?fS38+i9S&qyvA67hyMblLWM957lZ`3SQGFpqNf}@xkrTuo}F^ zWCUyh=WOmYGMogjF%dA-_Tw~oO%kw(m*6$E0*W_EUi48&0PLY=H77oI+B5>L5$`)t zrtG7Rc(1{0ngsN@{d5|xdyrxn>@#e?2zfznNRPdU*5wJ-=7GVV$yrxb- z?CobCHLE%9x{x~R1jKp*eteU+x)2wXjet;&{$2;K2~ofxg4cv3 z;1Jd02NAqxMnJJh_NwO^1{u6&egtgeZvobzgV)Rp@X>w_Q9VBMu&?0pDIf)3!kK{L z-&na=fR%!;VR{5?(PFu}0Y|7FpIO{y@0bqUR{Kf8x4@==6#Np5VC4l^N2nhE*jiNs zLXUu}wEs+33f=-*K%4fHg11N;0S{5ZKXO>l;4l%8f_KPBL^e@hYB5g=-UA~RkNy}D)@eo+c_?;laAWFa;;6}mk1`!5O0&W4fHJ|ewDdNyzU9CY0xCZ=O z^0`4o1t}?;#=U&CAKvL{t6<>F)82!;NuFmIV6bCW>Ua) z;BJN69uhBR@jTSK{03eFRJz(8_Ik_}aM3#WhfSdNq z(c%bsvTlTkECFu=U)e9G6pav(CE!)yf&Fq8;t1KAp445)BBTlch+=UcxMa7SrSMWj zo`5fbckGr^30{fF6EM4A%QudIzx&&RgSrdJBTv8;;1|2)7~vQ>BAgVFDF7gf#jilm zUO7f_Qbdt}uYtGhm16~`L=*{l5BStxIaYB(M3I0+7Hs)Ots;s9tN_0O=WP{KBge>D z)4t7}<_dsH7Hs*3RS`M@-UmLhQH*1pAX}4B5jp}kfgem3;W$P`=mGxXZ&bNskEv9It^gYgwtV9Rd9vPHW+S=+Y%bXHjip5B3OI{gxNCF4mT!!Z zt*Mj^tEfJ*!lp%`6dFG|0-PVwo zOlTsxB8&>qC3(-ONAZZd0GzF<%w-uNPu7no?{q>y8IlQsj;J1PP8y~}cqc%YC>9-L zYb$Dh7pdpn-Vw0t;t^669png5w@${bF8&4n^J_jMhnf>#(55XIspiUV!K>xDh!G<1~qV5vVmc@dTtU|PV7Tz8^Lat`>IQ51+UN&5XItULik0L3SOZ*0yYWZXR(Joxt=WYWL*KKE??j?O9ih8 z0?MdBclvu8=!6CB84=dbe*6@E&u=k literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_activity.png b/conversations/src/main/res/drawable-xxhdpi/ic_activity.png new file mode 100644 index 0000000000000000000000000000000000000000..0b642d9bb579d455b0f5bc235ac772eddde0f6f6 GIT binary patch literal 7209 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 z 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 literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_notification.png b/conversations/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..ee1e95346cac58354d9aec67888d39fad1167f2c GIT binary patch literal 2250 zcmV;*2sQVKP)2|x*W%uhJ=kCtT-TRn3J2Rc^>^HfY`Q16c-|x(s^E>za&N;tvBr%C#Ou5huR61%d z;M@*yIdC%NbJ_XD2?c3?}lC~(T~m|qM021sLqI0!tA*_d~WIL}kSXMq0zUj_bBlu;#>`l^xi z6-f;xoTEijQW29*N`f$Kc?&H%4s8r)atz!OD4*8sl<8iDCq_s;_z zm<@cpkbw*Fq$|<;{yWeN{0cagb$=>wC2$D%WFZ5`{jwhb-ol)|O2c{JTfi59lWrT# z@0ttj20k5o)EEZqB1uOj4ZB8gSs4s`p;x`6XC<9gE|QX(@MI&G6qnt8lQ-*vgN~j7|%yBXLJPffqj@W8i{-a)B=1T zxHRit5?F<8B`O&MzzX0v=Anam?EA>q+m2ANS&}+DDtb!N4HdA2;#_kiosjfDNjJsm z8?_g^A85#Gr+`O+KgIYJD*;{DV?KdxMe;_Nqc|N?&3SHn2=iE1Vm7EM5N3n!B{xjb z3*12!2?v4&bP=!xnC7;JF~b&1JtqQ!1hfg5mDTP6zFT^BDi3}(D1pU-8{M{mCxP{4 zfogX%dL931T3zkh$wm)sgPcV}nk zT@n1Rk_7-q*_AXSn`XRAY>ZL!BArgJavW!Y&quR}Sb{Y*HT8S;?D=w`b9Z!fe9>{7 z=^-R(azn2UR#-8L@e1g7k_N*F+S}XPfe-q8bUK|*?+fdjPN(~K?AVcT9LGXa!VxLE$LB7f0T4bgo?6r=gwQZySq>Nsdy@t>TYgs{x)!( zU*ElZ_tqaieE7i7(9n=i_u=i^xBn8j6YwoUXl-qMu)n`QIrVP0N=rZ(6u;;T65Ty{TQhcD=G~+qO2~bx8*T`p!%w z5;w0~w{FpzHEWt$T3VXw>gwu_969pF#*G^f^!4?f1zIJY3t)dW@LMvG(^v4MsJ#N( z2yDe7ieM4M%fkfZGU{ND<2d#1{Zv$6U`PaMz ze!KZwup;i6ZaazDme9xfM1`LQJr87C{i(oBC& z*RSkvZGRdNJ)A}(!-LjS9Cf!>iUGFpUYC-@5+v6Wuwmx**-35X~;Mq zmSs{$&?P&1kwaH30U>FP>|94rfS`tDaq6U`W0D?|v|iG*nDdfj*=vEJkpDE#6NTP1ev;?BxZYlfK}w~#Y%9}o<1jm!#-m@O>XM>vtuX< z(2}@Ulcakky(C*+NoAHuQg+lc&Uo*W^s1zXBrWv$e72+qB)#ow)qtc;0b?yu?s@y9 zr0Ll-<=(8;Vo$iwxv#=PDy70QEL*$<`zs7DrU37dNtB+(H2?V#g2~;y{kO}e-x7ff}L2kYZ^Nfw>$g8$t7l(gV~-=%)=hVq<|tYxx^yy Yf4cX#4H+3V&Hw-a07*qoM6N<$g89!cIsgCw literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_profile.png b/conversations/src/main/res/drawable-xxhdpi/ic_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..309dc51386e829580d61b203031a5e2540078104 GIT binary patch literal 2137 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 PaANRu^>bP0l+XkKb#<3p literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..e4439e7c9e7672433c3e4dbad339d5f7d73afbe1 GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^96&6^!3HF|1ZAaxl(DCaV~9p@a!NzP{{X4ENhvK} zO&K{4{!a)z@IjtqLuI4j3(pPT-9&rZm<=Qa=F5FZm0{qX5ym{pp#;^Kyf_5ul?S(PtLR*3J>lHg?zFjBCyeQ?@>f$2*IbDL%`3(yb- MPgg&ebxsLQ044q(hyVZp literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..566062f0d3eeb540aea2ecd6c0cbe3a58e4325e9 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^96-#+!3HFS!eXZZDHBf@#}J9|lF^9@hBV2c(IfrK82B??TrHSs{F6*K>ZA!u6{1- HoD!M<`O+a? literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png new file mode 100644 index 0000000000000000000000000000000000000000..432e68c4fc695e0d9506a4e0a67ae0ed0bc58dfd GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^96-#+!3HFS!eXZZDK$?Q#}J9|FVdQ&MBb@0J|m| AIsgCw literal 0 HcmV?d00001 diff --git a/conversations/src/main/res/drawable/actionbar_tab_indicator.xml b/conversations/src/main/res/drawable/actionbar_tab_indicator.xml new file mode 100644 index 000000000..5598ee424 --- /dev/null +++ b/conversations/src/main/res/drawable/actionbar_tab_indicator.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/es_slidingpane_shadow.xml b/conversations/src/main/res/drawable/es_slidingpane_shadow.xml new file mode 100644 index 000000000..44ffd4ea6 --- /dev/null +++ b/conversations/src/main/res/drawable/es_slidingpane_shadow.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/grey.xml b/conversations/src/main/res/drawable/grey.xml new file mode 100644 index 000000000..2e90d96d0 --- /dev/null +++ b/conversations/src/main/res/drawable/grey.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/greybackground.xml b/conversations/src/main/res/drawable/greybackground.xml new file mode 100644 index 000000000..bedc4b17a --- /dev/null +++ b/conversations/src/main/res/drawable/greybackground.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/infocard_border.xml b/conversations/src/main/res/drawable/infocard_border.xml new file mode 100644 index 000000000..af7d5d22b --- /dev/null +++ b/conversations/src/main/res/drawable/infocard_border.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/message_border.xml b/conversations/src/main/res/drawable/message_border.xml new file mode 100644 index 000000000..b35693d5c --- /dev/null +++ b/conversations/src/main/res/drawable/message_border.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/snackbar.xml b/conversations/src/main/res/drawable/snackbar.xml new file mode 100644 index 000000000..138186184 --- /dev/null +++ b/conversations/src/main/res/drawable/snackbar.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml new file mode 100644 index 000000000..a600118db --- /dev/null +++ b/conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml new file mode 100644 index 000000000..c3aa67ae6 --- /dev/null +++ b/conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml new file mode 100644 index 000000000..331fb1f06 --- /dev/null +++ b/conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml new file mode 100644 index 000000000..2744f38ef --- /dev/null +++ b/conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/account_row.xml b/conversations/src/main/res/layout/account_row.xml new file mode 100644 index 000000000..2d1190a3a --- /dev/null +++ b/conversations/src/main/res/layout/account_row.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/actionview_search.xml b/conversations/src/main/res/layout/actionview_search.xml new file mode 100644 index 000000000..64b75f0ed --- /dev/null +++ b/conversations/src/main/res/layout/actionview_search.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/activity_choose_contact.xml b/conversations/src/main/res/layout/activity_choose_contact.xml new file mode 100644 index 000000000..248a7822c --- /dev/null +++ b/conversations/src/main/res/layout/activity_choose_contact.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/activity_contact_details.xml b/conversations/src/main/res/layout/activity_contact_details.xml new file mode 100644 index 000000000..f7cb2198c --- /dev/null +++ b/conversations/src/main/res/layout/activity_contact_details.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/activity_edit_account.xml b/conversations/src/main/res/layout/activity_edit_account.xml new file mode 100644 index 000000000..97289628c --- /dev/null +++ b/conversations/src/main/res/layout/activity_edit_account.xml @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +