From 7382e3af9769f76fe4e19934a59e45a3f9858332 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Sun, 3 May 2015 22:25:46 +0200 Subject: renaming eu.siacs.conversations to de.thedevstack.conversationsplus "renaming eu.siacs.conversations to de.thedevstack.conversationsplus" package renaming completed --- manifest-merger-release-report.txt | 32 +- src/main/AndroidManifest.xml | 38 +- .../de/thedevstack/conversationsplus/Config.java | 81 + .../conversationsplus/crypto/OtrEngine.java | 303 +++ .../conversationsplus/crypto/PgpEngine.java | 369 +++ .../conversationsplus/crypto/sasl/DigestMd5.java | 91 + .../conversationsplus/crypto/sasl/Plain.java | 30 + .../crypto/sasl/SaslMechanism.java | 62 + .../conversationsplus/crypto/sasl/ScramSha1.java | 232 ++ .../conversationsplus/crypto/sasl/Tokenizer.java | 78 + .../conversationsplus/entities/AbstractEntity.java | 20 + .../conversationsplus/entities/Account.java | 411 ++++ .../conversationsplus/entities/Blockable.java | 11 + .../conversationsplus/entities/Bookmark.java | 160 ++ .../conversationsplus/entities/Contact.java | 505 ++++ .../conversationsplus/entities/Conversation.java | 770 +++++++ .../conversationsplus/entities/Downloadable.java | 28 + .../entities/DownloadableFile.java | 172 ++ .../entities/DownloadablePlaceholder.java | 39 + .../conversationsplus/entities/ListItem.java | 33 + .../conversationsplus/entities/Message.java | 587 +++++ .../conversationsplus/entities/MucOptions.java | 530 +++++ .../conversationsplus/entities/Presences.java | 90 + .../conversationsplus/entities/Roster.java | 91 + .../generator/AbstractGenerator.java | 91 + .../conversationsplus/generator/IqGenerator.java | 189 ++ .../generator/MessageGenerator.java | 187 ++ .../generator/PresenceGenerator.java | 57 + .../conversationsplus/http/HttpConnection.java | 310 +++ .../http/HttpConnectionManager.java | 28 + .../conversationsplus/parser/AbstractParser.java | 104 + .../conversationsplus/parser/IqParser.java | 158 ++ .../conversationsplus/parser/MessageParser.java | 653 ++++++ .../conversationsplus/parser/PresenceParser.java | 118 + .../persistance/DatabaseBackend.java | 400 ++++ .../conversationsplus/persistance/FileBackend.java | 525 +++++ .../persistance/OnPhoneContactsMerged.java | 5 + .../services/AbstractConnectionManager.java | 23 + .../conversationsplus/services/AvatarService.java | 295 +++ .../conversationsplus/services/EventReceiver.java | 24 + .../services/MessageArchiveService.java | 371 +++ .../services/NotificationService.java | 557 +++++ .../services/XmppConnectionService.java | 2434 ++++++++++++++++++++ .../conversationsplus/ui/AboutActivity.java | 15 + .../conversationsplus/ui/AboutPreference.java | 32 + .../ui/AbstractSearchableListItemActivity.java | 124 + .../conversationsplus/ui/BlockContactDialog.java | 41 + .../conversationsplus/ui/BlocklistActivity.java | 75 + .../ui/ChangePasswordActivity.java | 109 + .../ui/ChooseContactActivity.java | 152 ++ .../ui/ConferenceDetailsActivity.java | 562 +++++ .../ui/ContactDetailsActivity.java | 471 ++++ .../conversationsplus/ui/ConversationActivity.java | 1115 +++++++++ .../conversationsplus/ui/ConversationFragment.java | 1185 ++++++++++ .../conversationsplus/ui/EditAccountActivity.java | 505 ++++ .../conversationsplus/ui/EditMessage.java | 78 + .../ui/ManageAccountActivity.java | 273 +++ .../ui/PublishProfilePictureActivity.java | 253 ++ .../conversationsplus/ui/SettingsActivity.java | 92 + .../conversationsplus/ui/SettingsFragment.java | 65 + .../conversationsplus/ui/ShareWithActivity.java | 219 ++ .../ui/StartConversationActivity.java | 819 +++++++ .../conversationsplus/ui/TimePreference.java | 105 + .../conversationsplus/ui/UiCallback.java | 11 + .../conversationsplus/ui/VerifyOTRActivity.java | 446 ++++ .../conversationsplus/ui/XmppActivity.java | 957 ++++++++ .../ui/adapter/AccountAdapter.java | 54 + .../ui/adapter/ConversationAdapter.java | 224 ++ .../ui/adapter/KnownHostsAdapter.java | 71 + .../ui/adapter/ListItemAdapter.java | 181 ++ .../ui/adapter/MessageAdapter.java | 603 +++++ .../conversationsplus/utils/CryptoHelper.java | 124 + .../conversationsplus/utils/DNSHelper.java | 183 ++ .../conversationsplus/utils/ExceptionHandler.java | 46 + .../conversationsplus/utils/ExceptionHelper.java | 119 + .../conversationsplus/utils/ExifHelper.java | 161 ++ .../conversationsplus/utils/GeoHelper.java | 71 + .../utils/OnPhoneContactsLoadedListener.java | 9 + .../conversationsplus/utils/PRNGFixes.java | 327 +++ .../conversationsplus/utils/PhoneHelper.java | 107 + .../conversationsplus/utils/UIHelper.java | 366 +++ .../conversationsplus/utils/XmlHelper.java | 12 + .../thedevstack/conversationsplus/utils/Xmlns.java | 8 + .../conversationsplus/utils/XmppUri.java | 85 + .../thedevstack/conversationsplus/xml/Element.java | 171 ++ .../de/thedevstack/conversationsplus/xml/Tag.java | 104 + .../conversationsplus/xml/TagWriter.java | 114 + .../conversationsplus/xml/XmlReader.java | 141 ++ .../xmpp/OnAdvancedStreamFeaturesLoaded.java | 7 + .../conversationsplus/xmpp/OnBindListener.java | 7 + .../xmpp/OnContactStatusChanged.java | 7 + .../conversationsplus/xmpp/OnIqPacketReceived.java | 8 + .../xmpp/OnMessageAcknowledged.java | 7 + .../xmpp/OnMessagePacketReceived.java | 8 + .../xmpp/OnPresencePacketReceived.java | 8 + .../conversationsplus/xmpp/OnStatusChanged.java | 7 + .../conversationsplus/xmpp/OnUpdateBlocklist.java | 13 + .../conversationsplus/xmpp/PacketReceived.java | 5 + .../conversationsplus/xmpp/XmppConnection.java | 1125 +++++++++ .../xmpp/chatstate/ChatState.java | 32 + .../conversationsplus/xmpp/forms/Data.java | 85 + .../conversationsplus/xmpp/forms/Field.java | 50 + .../xmpp/jid/InvalidJidException.java | 49 + .../conversationsplus/xmpp/jid/Jid.java | 219 ++ .../xmpp/jingle/JingleCandidate.java | 147 ++ .../xmpp/jingle/JingleConnection.java | 971 ++++++++ .../xmpp/jingle/JingleConnectionManager.java | 164 ++ .../xmpp/jingle/JingleInbandTransport.java | 224 ++ .../xmpp/jingle/JingleSocks5Transport.java | 234 ++ .../xmpp/jingle/JingleTransport.java | 15 + .../jingle/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 | 96 + .../xmpp/jingle/stanzas/Reason.java | 13 + .../conversationsplus/xmpp/pep/Avatar.java | 73 + .../xmpp/stanzas/AbstractStanza.java | 54 + .../conversationsplus/xmpp/stanzas/IqPacket.java | 63 + .../xmpp/stanzas/MessagePacket.java | 69 + .../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 + .../xmpp/stanzas/streammgmt/RequestPacket.java | 12 + .../xmpp/stanzas/streammgmt/ResumePacket.java | 14 + src/main/java/eu/siacs/conversations/Config.java | 81 - .../eu/siacs/conversations/crypto/OtrEngine.java | 304 --- .../eu/siacs/conversations/crypto/PgpEngine.java | 369 --- .../siacs/conversations/crypto/sasl/DigestMd5.java | 91 - .../eu/siacs/conversations/crypto/sasl/Plain.java | 30 - .../conversations/crypto/sasl/SaslMechanism.java | 62 - .../siacs/conversations/crypto/sasl/ScramSha1.java | 232 -- .../siacs/conversations/crypto/sasl/Tokenizer.java | 78 - .../conversations/entities/AbstractEntity.java | 20 - .../eu/siacs/conversations/entities/Account.java | 411 ---- .../eu/siacs/conversations/entities/Blockable.java | 11 - .../eu/siacs/conversations/entities/Bookmark.java | 160 -- .../eu/siacs/conversations/entities/Contact.java | 505 ---- .../siacs/conversations/entities/Conversation.java | 770 ------- .../siacs/conversations/entities/Downloadable.java | 28 - .../conversations/entities/DownloadableFile.java | 172 -- .../entities/DownloadablePlaceholder.java | 39 - .../eu/siacs/conversations/entities/ListItem.java | 33 - .../eu/siacs/conversations/entities/Message.java | 587 ----- .../siacs/conversations/entities/MucOptions.java | 530 ----- .../eu/siacs/conversations/entities/Presences.java | 90 - .../eu/siacs/conversations/entities/Roster.java | 91 - .../conversations/generator/AbstractGenerator.java | 91 - .../siacs/conversations/generator/IqGenerator.java | 190 -- .../conversations/generator/MessageGenerator.java | 187 -- .../conversations/generator/PresenceGenerator.java | 57 - .../siacs/conversations/http/HttpConnection.java | 310 --- .../conversations/http/HttpConnectionManager.java | 28 - .../siacs/conversations/parser/AbstractParser.java | 109 - .../eu/siacs/conversations/parser/IqParser.java | 158 -- .../siacs/conversations/parser/MessageParser.java | 653 ------ .../siacs/conversations/parser/PresenceParser.java | 118 - .../conversations/persistance/DatabaseBackend.java | 400 ---- .../conversations/persistance/FileBackend.java | 525 ----- .../persistance/OnPhoneContactsMerged.java | 5 - .../services/AbstractConnectionManager.java | 23 - .../conversations/services/AvatarService.java | 295 --- .../conversations/services/EventReceiver.java | 24 - .../services/MessageArchiveService.java | 371 --- .../services/NotificationService.java | 559 ----- .../services/XmppConnectionService.java | 2434 -------------------- .../eu/siacs/conversations/ui/AboutActivity.java | 15 - .../eu/siacs/conversations/ui/AboutPreference.java | 33 - .../ui/AbstractSearchableListItemActivity.java | 124 - .../siacs/conversations/ui/BlockContactDialog.java | 41 - .../siacs/conversations/ui/BlocklistActivity.java | 75 - .../conversations/ui/ChangePasswordActivity.java | 110 - .../conversations/ui/ChooseContactActivity.java | 152 -- .../ui/ConferenceDetailsActivity.java | 563 ----- .../conversations/ui/ContactDetailsActivity.java | 471 ---- .../conversations/ui/ConversationActivity.java | 1116 --------- .../conversations/ui/ConversationFragment.java | 1187 ---------- .../conversations/ui/EditAccountActivity.java | 505 ---- .../eu/siacs/conversations/ui/EditMessage.java | 79 - .../conversations/ui/ManageAccountActivity.java | 273 --- .../ui/PublishProfilePictureActivity.java | 253 -- .../siacs/conversations/ui/SettingsActivity.java | 92 - .../siacs/conversations/ui/SettingsFragment.java | 65 - .../siacs/conversations/ui/ShareWithActivity.java | 222 -- .../ui/StartConversationActivity.java | 819 ------- .../eu/siacs/conversations/ui/TimePreference.java | 105 - .../java/eu/siacs/conversations/ui/UiCallback.java | 11 - .../siacs/conversations/ui/VerifyOTRActivity.java | 446 ---- .../eu/siacs/conversations/ui/XmppActivity.java | 957 -------- .../conversations/ui/adapter/AccountAdapter.java | 54 - .../ui/adapter/ConversationAdapter.java | 237 -- .../ui/adapter/KnownHostsAdapter.java | 71 - .../conversations/ui/adapter/ListItemAdapter.java | 181 -- .../conversations/ui/adapter/MessageAdapter.java | 604 ----- .../eu/siacs/conversations/utils/CryptoHelper.java | 124 - .../eu/siacs/conversations/utils/DNSHelper.java | 183 -- .../conversations/utils/ExceptionHandler.java | 46 - .../siacs/conversations/utils/ExceptionHelper.java | 119 - .../eu/siacs/conversations/utils/ExifHelper.java | 161 -- .../eu/siacs/conversations/utils/GeoHelper.java | 71 - .../utils/OnPhoneContactsLoadedListener.java | 9 - .../eu/siacs/conversations/utils/PRNGFixes.java | 327 --- .../eu/siacs/conversations/utils/PhoneHelper.java | 108 - .../eu/siacs/conversations/utils/UIHelper.java | 369 --- .../eu/siacs/conversations/utils/XmlHelper.java | 12 - .../java/eu/siacs/conversations/utils/Xmlns.java | 8 - .../java/eu/siacs/conversations/utils/XmppUri.java | 85 - .../java/eu/siacs/conversations/xml/Element.java | 171 -- src/main/java/eu/siacs/conversations/xml/Tag.java | 104 - .../java/eu/siacs/conversations/xml/TagWriter.java | 114 - .../java/eu/siacs/conversations/xml/XmlReader.java | 141 -- .../xmpp/OnAdvancedStreamFeaturesLoaded.java | 7 - .../siacs/conversations/xmpp/OnBindListener.java | 7 - .../conversations/xmpp/OnContactStatusChanged.java | 7 - .../conversations/xmpp/OnIqPacketReceived.java | 8 - .../conversations/xmpp/OnMessageAcknowledged.java | 7 - .../xmpp/OnMessagePacketReceived.java | 8 - .../xmpp/OnPresencePacketReceived.java | 8 - .../siacs/conversations/xmpp/OnStatusChanged.java | 7 - .../conversations/xmpp/OnUpdateBlocklist.java | 13 - .../siacs/conversations/xmpp/PacketReceived.java | 5 - .../siacs/conversations/xmpp/XmppConnection.java | 1125 --------- .../conversations/xmpp/chatstate/ChatState.java | 32 - .../eu/siacs/conversations/xmpp/forms/Data.java | 85 - .../eu/siacs/conversations/xmpp/forms/Field.java | 50 - .../xmpp/jid/InvalidJidException.java | 49 - .../java/eu/siacs/conversations/xmpp/jid/Jid.java | 219 -- .../conversations/xmpp/jingle/JingleCandidate.java | 147 -- .../xmpp/jingle/JingleConnection.java | 971 -------- .../xmpp/jingle/JingleConnectionManager.java | 164 -- .../xmpp/jingle/JingleInbandTransport.java | 224 -- .../xmpp/jingle/JingleSocks5Transport.java | 234 -- .../conversations/xmpp/jingle/JingleTransport.java | 15 - .../jingle/OnFileTransmissionStatusChanged.java | 9 - .../xmpp/jingle/OnJinglePacketReceived.java | 9 - .../xmpp/jingle/OnPrimaryCandidateFound.java | 6 - .../xmpp/jingle/OnTransportConnected.java | 7 - .../conversations/xmpp/jingle/stanzas/Content.java | 102 - .../xmpp/jingle/stanzas/JinglePacket.java | 96 - .../conversations/xmpp/jingle/stanzas/Reason.java | 13 - .../eu/siacs/conversations/xmpp/pep/Avatar.java | 73 - .../conversations/xmpp/stanzas/AbstractStanza.java | 54 - .../siacs/conversations/xmpp/stanzas/IqPacket.java | 63 - .../conversations/xmpp/stanzas/MessagePacket.java | 69 - .../conversations/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 - .../xmpp/stanzas/streammgmt/RequestPacket.java | 12 - .../xmpp/stanzas/streammgmt/ResumePacket.java | 14 - src/main/res/layout/activity_about.xml | 2 +- src/main/res/layout/fragment_conversation.xml | 4 +- src/main/res/xml/preferences.xml | 6 +- 257 files changed, 26221 insertions(+), 26258 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/Config.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/crypto/OtrEngine.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/crypto/sasl/DigestMd5.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Plain.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Tokenizer.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/AbstractEntity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Account.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Blockable.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Contact.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Downloadable.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/DownloadablePlaceholder.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Message.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Presences.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/Roster.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/generator/AbstractGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/generator/PresenceGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/http/HttpConnection.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/parser/AbstractParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/parser/IqParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/OnPhoneContactsMerged.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/AboutActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/AboutPreference.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/AbstractSearchableListItemActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/BlockContactDialog.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/BlocklistActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ChangePasswordActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ChooseContactActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ConferenceDetailsActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ContactDetailsActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/EditAccountActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/EditMessage.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ManageAccountActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/SettingsFragment.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/StartConversationActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/TimePreference.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/UiCallback.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/VerifyOTRActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/adapter/AccountAdapter.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/adapter/KnownHostsAdapter.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/CryptoHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHandler.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/ExifHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/OnPhoneContactsLoadedListener.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/PRNGFixes.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/PhoneHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/XmlHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/Xmlns.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/XmppUri.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xml/Element.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xml/Tag.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xml/TagWriter.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xml/XmlReader.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnAdvancedStreamFeaturesLoaded.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnBindListener.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnContactStatusChanged.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnIqPacketReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessageAcknowledged.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessagePacketReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnPresencePacketReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnStatusChanged.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/OnUpdateBlocklist.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/chatstate/ChatState.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Data.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Field.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jid/InvalidJidException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jid/Jid.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleCandidate.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleTransport.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnFileTransmissionStatusChanged.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnJinglePacketReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnPrimaryCandidateFound.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnTransportConnected.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Content.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/JinglePacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Reason.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/pep/Avatar.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/AbstractStanza.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/PresencePacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/ActivePacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/InactivePacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/AckPacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/EnablePacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/RequestPacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/ResumePacket.java delete mode 100644 src/main/java/eu/siacs/conversations/Config.java delete mode 100644 src/main/java/eu/siacs/conversations/crypto/OtrEngine.java delete mode 100644 src/main/java/eu/siacs/conversations/crypto/PgpEngine.java delete mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java delete mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java delete mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java delete mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java delete mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/AbstractEntity.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Account.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Blockable.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Bookmark.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Contact.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Conversation.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Downloadable.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/DownloadableFile.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/ListItem.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Message.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/MucOptions.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Presences.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/Roster.java delete mode 100644 src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java delete mode 100644 src/main/java/eu/siacs/conversations/generator/IqGenerator.java delete mode 100644 src/main/java/eu/siacs/conversations/generator/MessageGenerator.java delete mode 100644 src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java delete mode 100644 src/main/java/eu/siacs/conversations/http/HttpConnection.java delete mode 100644 src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java delete mode 100644 src/main/java/eu/siacs/conversations/parser/AbstractParser.java delete mode 100644 src/main/java/eu/siacs/conversations/parser/IqParser.java delete mode 100644 src/main/java/eu/siacs/conversations/parser/MessageParser.java delete mode 100644 src/main/java/eu/siacs/conversations/parser/PresenceParser.java delete mode 100644 src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java delete mode 100644 src/main/java/eu/siacs/conversations/persistance/FileBackend.java delete mode 100644 src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java delete mode 100644 src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java delete mode 100644 src/main/java/eu/siacs/conversations/services/AvatarService.java delete mode 100644 src/main/java/eu/siacs/conversations/services/EventReceiver.java delete mode 100644 src/main/java/eu/siacs/conversations/services/MessageArchiveService.java delete mode 100644 src/main/java/eu/siacs/conversations/services/NotificationService.java delete mode 100644 src/main/java/eu/siacs/conversations/services/XmppConnectionService.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/AboutActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/AboutPreference.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ConversationActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ConversationFragment.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/EditMessage.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/SettingsActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/SettingsFragment.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/TimePreference.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/UiCallback.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/XmppActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/CryptoHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/DNSHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/ExifHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/GeoHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/PRNGFixes.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/PhoneHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/UIHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/XmlHelper.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/Xmlns.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/XmppUri.java delete mode 100644 src/main/java/eu/siacs/conversations/xml/Element.java delete mode 100644 src/main/java/eu/siacs/conversations/xml/Tag.java delete mode 100644 src/main/java/eu/siacs/conversations/xml/TagWriter.java delete mode 100644 src/main/java/eu/siacs/conversations/xml/XmlReader.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/forms/Data.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/forms/Field.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java diff --git a/manifest-merger-release-report.txt b/manifest-merger-release-report.txt index b02dcf65..fb0c2ade 100644 --- a/manifest-merger-release-report.txt +++ b/manifest-merger-release-report.txt @@ -70,11 +70,11 @@ MERGED from com.android.support:support-v4:21.0.3:16:5 ADDED from AndroidManifest.xml:22:9 tools:replace ADDED from AndroidManifest.xml:23:9 -service#eu.siacs.conversations.services.XmppConnectionService +service#de.thedevstack.conversationsplus.services.XmppConnectionService ADDED from AndroidManifest.xml:24:9 android:name ADDED from AndroidManifest.xml:24:18 -receiver#eu.siacs.conversations.services.EventReceiver +receiver#de.thedevstack.conversationsplus.services.EventReceiver ADDED from AndroidManifest.xml:26:9 android:name ADDED from AndroidManifest.xml:26:19 @@ -92,7 +92,7 @@ action#android.intent.action.ACTION_SHUTDOWN ADDED from AndroidManifest.xml:30:17 android:name ADDED from AndroidManifest.xml:30:25 -activity#eu.siacs.conversations.ui.ConversationActivity +activity#de.thedevstack.conversationsplus.ui.ConversationActivity ADDED from AndroidManifest.xml:34:9 android:label ADDED from AndroidManifest.xml:36:13 @@ -112,7 +112,7 @@ category#android.intent.category.LAUNCHER ADDED from AndroidManifest.xml:42:17 android:name ADDED from AndroidManifest.xml:42:27 -activity#eu.siacs.conversations.ui.StartConversationActivity +activity#de.thedevstack.conversationsplus.ui.StartConversationActivity ADDED from AndroidManifest.xml:45:9 android:label ADDED from AndroidManifest.xml:48:13 @@ -150,31 +150,31 @@ action#android.nfc.action.NDEF_DISCOVERED ADDED from AndroidManifest.xml:66:17 android:name ADDED from AndroidManifest.xml:66:25 -activity#eu.siacs.conversations.ui.SettingsActivity +activity#de.thedevstack.conversationsplus.ui.SettingsActivity ADDED from AndroidManifest.xml:73:9 android:label ADDED from AndroidManifest.xml:75:13 android:name ADDED from AndroidManifest.xml:74:13 -activity#eu.siacs.conversations.ui.ChooseContactActivity +activity#de.thedevstack.conversationsplus.ui.ChooseContactActivity ADDED from AndroidManifest.xml:76:9 android:label ADDED from AndroidManifest.xml:78:13 android:name ADDED from AndroidManifest.xml:77:13 -activity#eu.siacs.conversations.ui.BlocklistActivity +activity#de.thedevstack.conversationsplus.ui.BlocklistActivity ADDED from AndroidManifest.xml:79:9 android:label ADDED from AndroidManifest.xml:81:13 android:name ADDED from AndroidManifest.xml:80:13 -activity#eu.siacs.conversations.ui.ChangePasswordActivity +activity#de.thedevstack.conversationsplus.ui.ChangePasswordActivity ADDED from AndroidManifest.xml:82:6 android:label ADDED from AndroidManifest.xml:84:7 android:name ADDED from AndroidManifest.xml:83:7 -activity#eu.siacs.conversations.ui.ManageAccountActivity +activity#de.thedevstack.conversationsplus.ui.ManageAccountActivity ADDED from AndroidManifest.xml:85:9 android:label ADDED from AndroidManifest.xml:88:13 @@ -182,13 +182,13 @@ ADDED from AndroidManifest.xml:85:9 ADDED from AndroidManifest.xml:87:13 android:name ADDED from AndroidManifest.xml:86:13 -activity#eu.siacs.conversations.ui.EditAccountActivity +activity#de.thedevstack.conversationsplus.ui.EditAccountActivity ADDED from AndroidManifest.xml:89:9 android:windowSoftInputMode ADDED from AndroidManifest.xml:91:13 android:name ADDED from AndroidManifest.xml:90:13 -activity#eu.siacs.conversations.ui.ConferenceDetailsActivity +activity#de.thedevstack.conversationsplus.ui.ConferenceDetailsActivity ADDED from AndroidManifest.xml:92:9 android:label ADDED from AndroidManifest.xml:94:13 @@ -196,7 +196,7 @@ ADDED from AndroidManifest.xml:92:9 ADDED from AndroidManifest.xml:95:13 android:name ADDED from AndroidManifest.xml:93:13 -activity#eu.siacs.conversations.ui.ContactDetailsActivity +activity#de.thedevstack.conversationsplus.ui.ContactDetailsActivity ADDED from AndroidManifest.xml:96:9 android:label ADDED from AndroidManifest.xml:98:13 @@ -204,7 +204,7 @@ ADDED from AndroidManifest.xml:96:9 ADDED from AndroidManifest.xml:99:13 android:name ADDED from AndroidManifest.xml:97:13 -activity#eu.siacs.conversations.ui.PublishProfilePictureActivity +activity#de.thedevstack.conversationsplus.ui.PublishProfilePictureActivity ADDED from AndroidManifest.xml:100:9 android:label ADDED from AndroidManifest.xml:102:13 @@ -212,7 +212,7 @@ ADDED from AndroidManifest.xml:100:9 ADDED from AndroidManifest.xml:103:13 android:name ADDED from AndroidManifest.xml:101:13 -activity#eu.siacs.conversations.ui.VerifyOTRActivity +activity#de.thedevstack.conversationsplus.ui.VerifyOTRActivity ADDED from AndroidManifest.xml:104:9 android:label ADDED from AndroidManifest.xml:106:13 @@ -220,7 +220,7 @@ ADDED from AndroidManifest.xml:104:9 ADDED from AndroidManifest.xml:107:13 android:name ADDED from AndroidManifest.xml:105:13 -activity#eu.siacs.conversations.ui.ShareWithActivity +activity#de.thedevstack.conversationsplus.ui.ShareWithActivity ADDED from AndroidManifest.xml:108:9 android:label ADDED from AndroidManifest.xml:110:13 @@ -242,7 +242,7 @@ MERGED from Conversations.libs:MemorizingTrustManager:unspecified:12:9 ADDED from AndroidManifest.xml:129:13 android:name ADDED from AndroidManifest.xml:127:13 -activity#eu.siacs.conversations.ui.AboutActivity +activity#de.thedevstack.conversationsplus.ui.AboutActivity ADDED from AndroidManifest.xml:130:9 android:label ADDED from AndroidManifest.xml:132:13 diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index e4e91d80..62821167 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ @@ -21,9 +21,9 @@ android:label="@string/app_name" android:theme="@style/ConversationsTheme" tools:replace="android:label" > - + - + @@ -32,7 +32,7 @@ @@ -43,7 +43,7 @@ @@ -71,42 +71,42 @@ @@ -128,12 +128,12 @@ android:theme="@style/ConversationsTheme" tools:replace="android:theme"/> + android:parentActivityName="de.thedevstack.conversationsplus.ui.SettingsActivity" > + android:value="de.thedevstack.conversationsplus.ui.SettingsActivity" /> diff --git a/src/main/java/de/thedevstack/conversationsplus/Config.java b/src/main/java/de/thedevstack/conversationsplus/Config.java new file mode 100644 index 00000000..2fb4f132 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/Config.java @@ -0,0 +1,81 @@ +package de.thedevstack.conversationsplus; + +import android.graphics.Bitmap; + +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; + +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 MINI_GRACE_PERIOD = 750; + + 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 UTF8_EMOTICONS = false; + + public static final int PAGE_SIZE = 50; + public static final int MAX_NUM_PAGES = 3; + + public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; + public static final int REFRESH_UI_INTERVAL = 500; + + public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb + public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance + + public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; + public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2; + public static final int MAM_MAX_MESSAGES = 500; + + public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE; + public static final int TYPING_TIMEOUT = 8; + + public static final String ENABLED_CIPHERS[] = { + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + + "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA", + + // Fallback. + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA384", + "TLS_RSA_WITH_AES_256_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA384", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + }; + + public static final String WEAK_CIPHER_PATTERNS[] = { + "_NULL_", + "_EXPORT_", + "_anon_", + "_RC4_", + "_DES_", + "_MD5", + }; + + private Config() { + + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/OtrEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/OtrEngine.java new file mode 100644 index 00000000..dfb64eaf --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/OtrEngine.java @@ -0,0 +1,303 @@ +package de.thedevstack.conversationsplus.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 de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.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.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; +import net.java.otr4j.session.InstanceTag; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.FragmenterInstructions; + +public class OtrEngine extends OtrCryptoEngineImpl 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 (final NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + } + + } + + @Override + public void askForSecret(SessionID id, InstanceTag instanceTag, String question) { + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account,jid); + if (conversation!=null) { + conversation.smp().hint = question; + conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED; + mXmppConnectionService.updateConversationUi(); + } + } catch (InvalidJidException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString()); + } + } + + @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) { + try { + return getFingerprintRaw(getPublicKey()); + } catch (OtrCryptoException e) { + return null; + } + } + + public PublicKey getPublicKey() { + if (this.keyPair == null) { + return null; + } + return this.keyPair.getPublic(); + } + + @Override + public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { + if (this.keyPair == null) { + KeyPairGenerator kg; + try { + kg = KeyPairGenerator.getInstance("DSA"); + this.keyPair = kg.genKeyPair(); + this.saveKey(); + mXmppConnectionService.databaseBackend.updateAccount(account); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, + "error generating key pair " + e.getMessage()); + } + } + return this.keyPair; + } + + @Override + public String getReplyForUnreadableMessage(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public OtrPolicy getSessionPolicy(SessionID arg0) { + return otrPolicy; + } + + @Override + public void injectMessage(SessionID session, String body) + throws OtrException { + MessagePacket packet = new MessagePacket(); + packet.setFrom(account.getJid()); + if (session.getUserID().isEmpty()) { + packet.setAttribute("to", session.getAccountID()); + } else { + packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); + } + packet.setBody(body); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.addChild("no-store", "urn:xmpp:hints"); + + try { + Jid jid = Jid.fromSessionID(session); + Conversation conversation = mXmppConnectionService.find(account,jid); + if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (mXmppConnectionService.sendChatStates()) { + packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); + } + } + } catch (final InvalidJidException ignored) { + + } + + packet.setType(MessagePacket.TYPE_CHAT); + account.getXmppConnection().sendMessagePacket(packet); + } + + @Override + public void messageFromAnotherInstanceReceived(SessionID session) { + try { + Jid jid = Jid.fromSessionID(session); + Conversation conversation = mXmppConnectionService.find(account, jid); + String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId(); + if (id != null) { + MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateOtrError(jid,id); + packet.setFrom(account.getJid()); + mXmppConnectionService.sendMessagePacket(account,packet); + Log.d(Config.LOGTAG,packet.toString()); + Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": unreadable OTR message in "+conversation.getName()); + } + } catch (InvalidJidException e) { + return; + } + } + + @Override + public void multipleInstancesDetected(SessionID arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void requireEncryptedMessage(SessionID arg0, String arg1) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void showError(SessionID arg0, String arg1) throws OtrException { + Log.d(Config.LOGTAG,"show error"); + } + + @Override + public void smpAborted(SessionID id) throws OtrException { + setSmpStatus(id, Conversation.Smp.STATUS_NONE); + } + + private void setSmpStatus(SessionID id, int status) { + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account,jid); + if (conversation!=null) { + conversation.smp().status = status; + mXmppConnectionService.updateConversationUi(); + } + } catch (final InvalidJidException ignored) { + + } + } + + @Override + public void smpError(SessionID id, int arg1, boolean arg2) + throws OtrException { + setSmpStatus(id, Conversation.Smp.STATUS_NONE); + } + + @Override + public void unencryptedMessageReceived(SessionID arg0, String arg1) + throws OtrException { + throw new OtrException(new Exception("unencrypted message received")); + } + + @Override + public void unreadableMessageReceived(SessionID arg0) throws OtrException { + Log.d(Config.LOGTAG,"unreadable message received"); + throw new OtrException(new Exception("unreadable message received")); + } + + @Override + public void unverify(SessionID id, String arg1) { + setSmpStatus(id, Conversation.Smp.STATUS_FAILED); + } + + @Override + public void verify(SessionID id, String fingerprint, boolean approved) { + Log.d(Config.LOGTAG,"OtrEngine.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")"); + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account,jid); + if (conversation!=null) { + if (approved) { + conversation.getContact().addOtrFingerprint(fingerprint); + } + conversation.smp().hint = null; + conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); + } + } catch (final InvalidJidException ignored) { + } + } + + @Override + public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) { + return null; + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java new file mode 100644 index 00000000..b7fa9585 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java @@ -0,0 +1,369 @@ +package de.thedevstack.conversationsplus.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.http.HttpConnectionManager; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.ui.UiCallback; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; + +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) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid().toBareJid().toString()); + 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); + final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); + if (message.trusted() + && Settings.DOWNLOAD_IMAGE_LINKS + && mXmppConnectionService.isDownloadAllowedInConnection() + && message.bodyContainsDownloadable() + && manager.getAutoAcceptFileSize() > 0) { + manager.createNewConnection(message); + } + 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: + callback.error(R.string.openpgp_error, message); + } + } + }); + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + try { + final DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + final DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + outputFile.getParentFile().mkdirs(); + 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: + URL url = message.getImageParams().url; + mXmppConnectionService.getFileBackend().updateFileParams(message,url); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + PgpEngine.this.mXmppConnectionService + .updateMessage(message); + inputFile.delete(); + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(outputFile)); + mXmppConnectionService.sendBroadcast(intent); + 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); + } + } + }); + } catch (final 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().toBareJid().toString()); + + 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]); + } + } + 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 || message.getType() == Message.TYPE_FILE) { + try { + DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + outputFile.getParentFile().mkdirs(); + 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 (final IOException e) { + callback.error(R.string.openpgp_error, message); + } + } + } + + public long fetchKeyId(Account account, String status, String signature) { + if ((signature == null) || (api == null)) { + return 0; + } + if (status == null) { + status = ""; + } + final 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().toBareJid().toString()); + 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: + 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().toBareJid().toString()); + 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); + } + } + }); + } + + 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().toBareJid().toString()); + 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); + } + } + }); + } + + 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().toBareJid().toString()); + 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().toBareJid().toString()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/DigestMd5.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/DigestMd5.java new file mode 100644 index 00000000..f4c3f28e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/DigestMd5.java @@ -0,0 +1,91 @@ +package de.thedevstack.conversationsplus.crypto.sasl; + +import android.util.Base64; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.xml.TagWriter; + +public class DigestMd5 extends SaslMechanism { + public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + } + + @Override + public int getPriority() { + return 10; + } + + @Override + public String getMechanism() { + return "DIGEST-MD5"; + } + + private State state = State.INITIAL; + + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case INITIAL: + state = State.RESPONSE_SENT; + final String encodedResponse; + try { + final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); + String nonce = ""; + for (final String token : tokenizer) { + final String[] parts = token.split("=", 2); + if (parts[0].equals("nonce")) { + nonce = parts[1].replace("\"", ""); + } else if (parts[0].equals("rspauth")) { + return ""; + } + } + final String digestUri = "xmpp/" + account.getServer(); + final String nonceCount = "00000001"; + final String x = account.getUsername() + ":" + account.getServer() + ":" + + account.getPassword(); + final MessageDigest md = MessageDigest.getInstance("MD5"); + final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); + final String cNonce = new BigInteger(100, rng).toString(32); + final byte[] a1 = CryptoHelper.concatenateByteArrays(y, + (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); + final String a2 = "AUTHENTICATE:" + digestUri; + final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); + final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset + .defaultCharset()))); + final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + + ":auth:" + ha2; + final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset + .defaultCharset()))); + final String saslString = "username=\"" + account.getUsername() + + "\",realm=\"" + account.getServer() + "\",nonce=\"" + + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount + + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" + + response + ",charset=utf-8"; + encodedResponse = Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } catch (final NoSuchAlgorithmException e) { + throw new AuthenticationException(e); + } + + return encodedResponse; + case RESPONSE_SENT: + state = State.VALID_SERVER_RESPONSE; + break; + case VALID_SERVER_RESPONSE: + if (challenge==null) { + return null; //everything is fine + } + default: + throw new InvalidStateException(state); + } + return null; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Plain.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Plain.java new file mode 100644 index 00000000..179c12de --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Plain.java @@ -0,0 +1,30 @@ +package de.thedevstack.conversationsplus.crypto.sasl; + +import android.util.Base64; + +import java.nio.charset.Charset; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xml.TagWriter; + +public class Plain extends SaslMechanism { + public Plain(final TagWriter tagWriter, final Account account) { + super(tagWriter, account, null); + } + + @Override + public int getPriority() { + return 10; + } + + @Override + public String getMechanism() { + return "PLAIN"; + } + + @Override + public String getClientFirstMessage() { + final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword(); + return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java new file mode 100644 index 00000000..ed2764c8 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java @@ -0,0 +1,62 @@ +package de.thedevstack.conversationsplus.crypto.sasl; + +import java.security.SecureRandom; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xml.TagWriter; + +public abstract class SaslMechanism { + + final protected TagWriter tagWriter; + final protected Account account; + final protected SecureRandom rng; + + protected static enum State { + INITIAL, + AUTH_TEXT_SENT, + RESPONSE_SENT, + VALID_SERVER_RESPONSE, + } + + public static class AuthenticationException extends Exception { + public AuthenticationException(final String message) { + super(message); + } + + public AuthenticationException(final Exception inner) { + super(inner); + } + } + + public static class InvalidStateException extends AuthenticationException { + public InvalidStateException(final String message) { + super(message); + } + + public InvalidStateException(final State state) { + this("Invalid state: " + state.toString()); + } + } + + public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + this.tagWriter = tagWriter; + this.account = account; + this.rng = rng; + } + + /** + * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another + * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade + * attacks). + * @return An arbitrary int representing the priority + */ + public abstract int getPriority(); + + public abstract String getMechanism(); + public String getClientFirstMessage() { + return ""; + } + public String getResponse(final String challenge) throws AuthenticationException { + return ""; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java new file mode 100644 index 00000000..af77771c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java @@ -0,0 +1,232 @@ +package de.thedevstack.conversationsplus.crypto.sasl; + +import android.util.Base64; +import android.util.LruCache; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.SecureRandom; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.xml.TagWriter; + +public class ScramSha1 extends SaslMechanism { + // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. + final private static String GS2_HEADER = "n,,"; + private String clientFirstMessageBare; + private byte[] serverFirstMessage; + final private String clientNonce; + private byte[] serverSignature = null; + private static HMac HMAC; + private static Digest DIGEST; + private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); + private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); + + public static class KeyPair { + final public byte[] clientKey; + final public byte[] serverKey; + + public KeyPair(final byte[] clientKey, final byte[] serverKey) { + this.clientKey = clientKey; + this.serverKey = serverKey; + } + } + + private static final LruCache CACHE; + + static { + DIGEST = new SHA1Digest(); + HMAC = new HMac(new SHA1Digest()); + CACHE = new LruCache(10) { + protected KeyPair create(final String k) { + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + // Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()' + // is applied to prevent commas in the strings breaking things. + final String[] kparts = k.split(",", 4); + try { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), + Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + + return new KeyPair(clientKey, serverKey); + } catch (final InvalidKeyException | NumberFormatException e) { + return null; + } + } + }; + } + + private State state = State.INITIAL; + + public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + + // This nonce should be different for each authentication attempt. + clientNonce = new BigInteger(100, this.rng).toString(32); + clientFirstMessageBare = ""; + } + + @Override + public int getPriority() { + return 20; + } + + @Override + public String getMechanism() { + return "SCRAM-SHA-1"; + } + + @Override + public String getClientFirstMessage() { + if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { + clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + + ",r=" + this.clientNonce; + state = State.AUTH_TEXT_SENT; + } + return Base64.encodeToString( + (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } + + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case AUTH_TEXT_SENT: + serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); + final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); + String nonce = ""; + int iterationCount = -1; + String salt = ""; + for (final String token : tokenizer) { + if (token.charAt(1) == '=') { + switch (token.charAt(0)) { + case 'i': + try { + iterationCount = Integer.parseInt(token.substring(2)); + } catch (final NumberFormatException e) { + throw new AuthenticationException(e); + } + break; + case 's': + salt = token.substring(2); + break; + case 'r': + nonce = token.substring(2); + break; + case 'm': + /* + * RFC 5802: + * m: This attribute is reserved for future extensibility. In this + * version of SCRAM, its presence in a client or a server message + * MUST cause authentication failure when the attribute is parsed by + * the other end. + */ + throw new AuthenticationException("Server sent reserved token: `m'"); + } + } + } + + if (iterationCount < 0) { + throw new AuthenticationException("Server did not send iteration count"); + } + if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { + throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); + } + if (salt.isEmpty()) { + throw new AuthenticationException("Server sent empty salt"); + } + + final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( + GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; + final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' + + clientFinalMessageWithoutProof).getBytes(); + + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + final KeyPair keys = CACHE.get( + CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," + + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," + + CryptoHelper.bytesToHex(salt.getBytes()) + "," + + String.valueOf(iterationCount) + ); + if (keys == null) { + throw new AuthenticationException("Invalid keys generated"); + } + final byte[] clientSignature; + try { + serverSignature = hmac(keys.serverKey, authMessage); + final byte[] storedKey = digest(keys.clientKey); + + clientSignature = hmac(storedKey, authMessage); + + } catch (final InvalidKeyException e) { + throw new AuthenticationException(e); + } + + final byte[] clientProof = new byte[keys.clientKey.length]; + + for (int i = 0; i < clientProof.length; i++) { + clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); + } + + + final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + + Base64.encodeToString(clientProof, Base64.NO_WRAP); + state = State.RESPONSE_SENT; + return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); + case RESPONSE_SENT: + final String clientCalculatedServerFinalMessage = "v=" + + Base64.encodeToString(serverSignature, Base64.NO_WRAP); + if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { + throw new AuthenticationException("Server final message does not match calculated final message"); + } + state = State.VALID_SERVER_RESPONSE; + return ""; + default: + throw new InvalidStateException(state); + } + } + + public static synchronized byte[] hmac(final byte[] key, final byte[] input) + throws InvalidKeyException { + HMAC.init(new KeyParameter(key)); + HMAC.update(input, 0, input.length); + final byte[] out = new byte[HMAC.getMacSize()]; + HMAC.doFinal(out, 0); + return out; + } + + public static synchronized byte[] digest(byte[] bytes) { + DIGEST.reset(); + DIGEST.update(bytes, 0, bytes.length); + final byte[] out = new byte[DIGEST.getDigestSize()]; + DIGEST.doFinal(out, 0); + return out; + } + + /* + * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the + * pseudorandom function (PRF) and with dkLen == output length of + * HMAC() == output length of H(). + */ + private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) + throws InvalidKeyException { + byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); + byte[] out = u.clone(); + for (int i = 1; i < iterations; i++) { + u = hmac(key, u); + for (int j = 0; j < u.length; j++) { + out[j] ^= u[j]; + } + } + return out; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Tokenizer.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Tokenizer.java new file mode 100644 index 00000000..cc2805de --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/Tokenizer.java @@ -0,0 +1,78 @@ +package de.thedevstack.conversationsplus.crypto.sasl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * A tokenizer for GS2 header strings + */ +public final class Tokenizer implements Iterator, Iterable { + private final List parts; + private int index; + + public Tokenizer(final byte[] challenge) { + final String challengeString = new String(challenge); + parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); + // Trim parts. + for (int i = 0; i < parts.size(); i++) { + parts.set(i, parts.get(i).trim()); + } + index = 0; + } + + /** + * Returns true if there is at least one more element, false otherwise. + * + * @see #next + */ + @Override + public boolean hasNext() { + return parts.size() != index + 1; + } + + /** + * Returns the next object and advances the iterator. + * + * @return the next object. + * @throws java.util.NoSuchElementException if there are no more elements. + * @see #hasNext + */ + @Override + public String next() { + if (hasNext()) { + return parts.get(index++); + } else { + throw new NoSuchElementException("No such element. Size is: " + parts.size()); + } + } + + /** + * Removes the last object returned by {@code next} from the collection. + * This method can only be called once between each call to {@code next}. + * + * @throws UnsupportedOperationException if removing is not supported by the collection being + * iterated. + * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has + * already been called after the last call to {@code next}. + */ + @Override + public void remove() { + if(index <= 0) { + throw new IllegalStateException("You can't delete an element before first next() method call"); + } + parts.remove(--index); + } + + /** + * Returns an {@link java.util.Iterator} for the elements in this object. + * + * @return An {@code Iterator} instance. + */ + @Override + public Iterator iterator() { + return parts.iterator(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/AbstractEntity.java b/src/main/java/de/thedevstack/conversationsplus/entities/AbstractEntity.java new file mode 100644 index 00000000..ebc0a8d5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/AbstractEntity.java @@ -0,0 +1,20 @@ +package de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/entities/Account.java b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java new file mode 100644 index 00000000..1b6539da --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java @@ -0,0 +1,411 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.PublicKey; +import java.security.interfaces.DSAPublicKey; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.OtrEngine; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xmpp.XmppConnection; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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 String PINNED_MECHANISM_KEY = "pinned_mechanism"; + + 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 enum State { + DISABLED, + OFFLINE, + CONNECTING, + ONLINE, + NO_INTERNET, + UNAUTHORIZED(true), + SERVER_NOT_FOUND(true), + REGISTRATION_FAILED(true), + REGISTRATION_CONFLICT(true), + REGISTRATION_SUCCESSFUL, + REGISTRATION_NOT_SUPPORTED(true), + SECURITY_ERROR(true), + INCOMPATIBLE_SERVER(true); + + private final boolean isError; + + public boolean isError() { + return this.isError; + } + + private State(final boolean isError) { + this.isError = isError; + } + + private State() { + this(false); + } + + public int getReadableId() { + switch (this) { + case DISABLED: + return R.string.account_status_disabled; + case ONLINE: + return R.string.account_status_online; + case CONNECTING: + return R.string.account_status_connecting; + case OFFLINE: + return R.string.account_status_offline; + case UNAUTHORIZED: + return R.string.account_status_unauthorized; + case SERVER_NOT_FOUND: + return R.string.account_status_not_found; + case NO_INTERNET: + return R.string.account_status_no_internet; + case REGISTRATION_FAILED: + return R.string.account_status_regis_fail; + case REGISTRATION_CONFLICT: + return R.string.account_status_regis_conflict; + case REGISTRATION_SUCCESSFUL: + return R.string.account_status_regis_success; + case REGISTRATION_NOT_SUPPORTED: + return R.string.account_status_regis_not_sup; + case SECURITY_ERROR: + return R.string.account_status_security_error; + case INCOMPATIBLE_SERVER: + return R.string.account_status_incompatible_server; + default: + return R.string.account_status_unknown; + } + } + } + + public List pendingConferenceJoins = new CopyOnWriteArrayList<>(); + public List pendingConferenceLeaves = new CopyOnWriteArrayList<>(); + protected Jid jid; + protected String password; + protected int options = 0; + protected String rosterVersion; + protected State status = State.OFFLINE; + protected JSONObject keys = new JSONObject(); + protected String avatar; + protected boolean online = false; + private OtrEngine otrEngine = null; + private XmppConnection xmppConnection = null; + private long mEndGracePeriod = 0L; + private String otrFingerprint; + private final Roster roster = new Roster(this); + private List bookmarks = new CopyOnWriteArrayList<>(); + private final Collection blocklist = new CopyOnWriteArraySet<>(); + + public Account() { + this.uuid = "0"; + } + + public Account(final Jid jid, final String password) { + this(java.util.UUID.randomUUID().toString(), jid, + password, 0, null, "", null); + } + + public Account(final String uuid, final Jid jid, + final String password, final int options, final String rosterVersion, final String keys, + final String avatar) { + this.uuid = uuid; + this.jid = jid; + if (jid.isBareJid()) { + this.setResource("mobile"); + } + this.password = password; + this.options = options; + this.rosterVersion = rosterVersion; + try { + this.keys = new JSONObject(keys); + } catch (final JSONException ignored) { + this.keys = new JSONObject(); + } + this.avatar = avatar; + } + + public static Account fromCursor(final Cursor cursor) { + Jid jid = null; + try { + jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)), + cursor.getString(cursor.getColumnIndex(SERVER)), "mobile"); + } catch (final InvalidJidException ignored) { + } + return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + jid, + 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 boolean isOptionSet(final int option) { + return ((options & (1 << option)) != 0); + } + + public void setOption(final int option, final boolean value) { + if (value) { + this.options |= 1 << option; + } else { + this.options &= ~(1 << option); + } + } + + public String getUsername() { + return jid.getLocalpart(); + } + + public void setUsername(final String username) throws InvalidJidException { + jid = Jid.fromParts(username, jid.getDomainpart(), jid.getResourcepart()); + } + + public Jid getServer() { + return jid.toDomainJid(); + } + + public void setServer(final String server) throws InvalidJidException { + jid = Jid.fromParts(jid.getLocalpart(), server, jid.getResourcepart()); + } + + public String getPassword() { + return password; + } + + public void setPassword(final String password) { + this.password = password; + } + + public State getStatus() { + if (isOptionSet(OPTION_DISABLED)) { + return State.DISABLED; + } else { + return this.status; + } + } + + public void setStatus(final State status) { + this.status = status; + } + + public boolean errorStatus() { + return getStatus().isError(); + } + + public boolean hasErrorStatus() { + return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 2; + } + + public String getResource() { + return jid.getResourcepart(); + } + + public void setResource(final String resource) { + try { + jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource); + } catch (final InvalidJidException ignored) { + } + } + + public Jid getJid() { + return jid; + } + + public JSONObject getKeys() { + return keys; + } + + public boolean setKey(final String keyName, final String keyValue) { + try { + this.keys.put(keyName, keyValue); + return true; + } catch (final JSONException e) { + return false; + } + } + + @Override + public ContentValues getContentValues() { + final ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(USERNAME, jid.getLocalpart()); + values.put(SERVER, jid.getDomainpart()); + 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 void initOtrEngine(final XmppConnectionService context) { + this.otrEngine = new OtrEngine(context, this); + } + + public OtrEngine getOtrEngine() { + return this.otrEngine; + } + + public XmppConnection getXmppConnection() { + return this.xmppConnection; + } + + public void setXmppConnection(final XmppConnection connection) { + this.xmppConnection = connection; + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + if (this.otrEngine == null) { + return null; + } + final PublicKey publicKey = this.otrEngine.getPublicKey(); + if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { + return null; + } + this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey); + return this.otrFingerprint; + } catch (final OtrCryptoException ignored) { + return null; + } + } else { + return this.otrFingerprint; + } + } + + public String getRosterVersion() { + if (this.rosterVersion == null) { + return ""; + } else { + return this.rosterVersion; + } + } + + public void setRosterVersion(final String version) { + this.rosterVersion = version; + } + + public int countPresences() { + return this.getRoster().getContact(this.getJid().toBareJid()).getPresences().size(); + } + + public String getPgpSignature() { + if (keys.has("pgp_signature")) { + try { + return keys.getString("pgp_signature"); + } catch (final JSONException e) { + return null; + } + } else { + return null; + } + } + + public Roster getRoster() { + return this.roster; + } + + public List getBookmarks() { + return this.bookmarks; + } + + public void setBookmarks(final List bookmarks) { + this.bookmarks = bookmarks; + } + + public boolean hasBookmarkFor(final Jid conferenceJid) { + for (final Bookmark bookmark : this.bookmarks) { + final Jid jid = bookmark.getJid(); + if (jid != null && jid.equals(conferenceJid.toBareJid())) { + return true; + } + } + return false; + } + + public boolean setAvatar(final 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 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; + } + + public String getShareableUri() { + final String fingerprint = this.getOtrFingerprint(); + if (fingerprint != null) { + return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint; + } else { + return "xmpp:" + this.getJid().toBareJid().toString(); + } + } + + public boolean isBlocked(final ListItem contact) { + final Jid jid = contact.getJid(); + return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid())); + } + + public boolean isBlocked(final Jid jid) { + return jid != null && blocklist.contains(jid.toBareJid()); + } + + public Collection getBlocklist() { + return this.blocklist; + } + + public void clearBlocklist() { + getBlocklist().clear(); + } + + public boolean isOnlineAndConnected() { + return this.getStatus() == State.ONLINE && this.getXmppConnection() != null; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Blockable.java b/src/main/java/de/thedevstack/conversationsplus/entities/Blockable.java new file mode 100644 index 00000000..beff901d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Blockable.java @@ -0,0 +1,11 @@ +package de.thedevstack.conversationsplus.entities; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public interface Blockable { + public boolean isBlocked(); + public boolean isDomainBlocked(); + public Jid getBlockedJid(); + public Jid getJid(); + public Account getAccount(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java new file mode 100644 index 00000000..ea7df2f2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java @@ -0,0 +1,160 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class Bookmark extends Element implements ListItem { + + private Account account; + private Conversation mJoinedConversation; + + public Bookmark(final Account account, final Jid jid) { + super("conference"); + this.setAttribute("jid", jid.toString()); + 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"); + } + } + + @Override + public int compareTo(final 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().getLocalpart(); + } + } + + @Override + public Jid getJid() { + return this.getAttributeAsJid("jid"); + } + + @Override + public List getTags() { + ArrayList tags = new ArrayList(); + for (Element element : getChildren()) { + if (element.getName().equals("group") && element.getContent() != null) { + String group = element.getContent(); + tags.add(new Tag(group, UIHelper.getColorForName(group))); + } + } + return tags; + } + + public String getNick() { + Element nick = this.findChild("nick"); + if (nick != null) { + return nick.getContent(); + } else { + return null; + } + } + + public void setNick(String nick) { + Element element = this.findChild("nick"); + if (element == null) { + element = this.addChild("nick"); + } + element.setContent(nick); + } + + public boolean autojoin() { + return this.getAttributeAsBoolean("autojoin"); + } + + public String getPassword() { + Element password = this.findChild("password"); + if (password != null) { + return password.getContent(); + } else { + return null; + } + } + + public void setPassword(String password) { + Element element = this.findChild("password"); + if (element != null) { + element.setContent(password); + } + } + + public boolean match(String needle) { + if (needle == null) { + return true; + } + needle = needle.toLowerCase(Locale.US); + final Jid jid = getJid(); + return (jid != null && jid.toString().contains(needle)) || + getDisplayName().toLowerCase(Locale.US).contains(needle) || + matchInTag(needle); + } + + private boolean matchInTag(String needle) { + needle = needle.toLowerCase(Locale.US); + for (Tag tag : getTags()) { + if (tag.getName().toLowerCase(Locale.US).contains(needle)) { + return true; + } + } + return false; + } + + public Account getAccount() { + return this.account; + } + + public Conversation getConversation() { + return this.mJoinedConversation; + } + + public void setConversation(Conversation conversation) { + this.mJoinedConversation = conversation; + } + + public String getName() { + return this.getAttribute("name"); + } + + public void setName(String name) { + this.name = name; + } + + public void unregisterConversation() { + if (this.mJoinedConversation != null) { + this.mJoinedConversation.deregisterWithBookmark(); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java new file mode 100644 index 00000000..07d42d28 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java @@ -0,0 +1,505 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class Contact implements ListItem, Blockable { + 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"; + public static final String LAST_PRESENCE = "last_presence"; + public static final String LAST_TIME = "last_time"; + public static final String GROUPS = "groups"; + public Lastseen lastseen = new Lastseen(); + protected String accountUuid; + protected String systemName; + protected String serverName; + protected String presenceName; + protected Jid jid; + protected int subscription = 0; + protected String systemAccount; + protected String photoUri; + protected String avatar; + protected JSONObject keys = new JSONObject(); + protected JSONArray groups = new JSONArray(); + protected Presences presences = new Presences(); + protected Account account; + + public Contact(final String account, final String systemName, final String serverName, + final Jid jid, final int subscription, final String photoUri, + final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) { + this.accountUuid = account; + this.systemName = systemName; + this.serverName = serverName; + this.jid = jid; + this.subscription = subscription; + this.photoUri = photoUri; + this.systemAccount = systemAccount; + try { + this.keys = (keys == null ? new JSONObject("") : new JSONObject(keys)); + } catch (JSONException e) { + this.keys = new JSONObject(); + } + this.avatar = avatar; + try { + this.groups = (groups == null ? new JSONArray() : new JSONArray(groups)); + } catch (JSONException e) { + this.groups = new JSONArray(); + } + this.lastseen = lastseen; + } + + public Contact(final Jid jid) { + this.jid = jid; + } + + public static Contact fromCursor(final Cursor cursor) { + final Lastseen lastseen = new Lastseen( + cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)), + cursor.getLong(cursor.getColumnIndex(LAST_TIME))); + final Jid jid; + try { + jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true); + } catch (final InvalidJidException e) { + // TODO: Borked DB... handle this somehow? + return null; + } + return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), + cursor.getString(cursor.getColumnIndex(SERVERNAME)), + 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)), + lastseen, + cursor.getString(cursor.getColumnIndex(GROUPS))); + } + + 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 if (jid.hasLocalpart()) { + return jid.getLocalpart(); + } else { + return jid.getDomainpart(); + } + } + + public String getProfilePhoto() { + return this.photoUri; + } + + public Jid getJid() { + return jid; + } + + @Override + public List getTags() { + final ArrayList tags = new ArrayList<>(); + for (final String group : getGroups()) { + tags.add(new Tag(group, UIHelper.getColorForName(group))); + } + switch (getMostAvailableStatus()) { + case Presences.CHAT: + case Presences.ONLINE: + tags.add(new Tag("online", 0xff259b24)); + break; + case Presences.AWAY: + tags.add(new Tag("away", 0xffff9800)); + break; + case Presences.XA: + tags.add(new Tag("not available", 0xffe51c23)); + break; + case Presences.DND: + tags.add(new Tag("dnd", 0xffe51c23)); + break; + } + if (isBlocked()) { + tags.add(new Tag("blocked", 0xff2e2f3b)); + } + return tags; + } + + public boolean match(String needle) { + if (needle == null || needle.isEmpty()) { + return true; + } + needle = needle.toLowerCase(Locale.US).trim(); + String[] parts = needle.split("\\s+"); + if (parts.length > 1) { + for(int i = 0; i < parts.length; ++i) { + if (!match(parts[i])) { + return false; + } + } + return true; + } else { + return jid.toString().contains(needle) || + getDisplayName().toLowerCase(Locale.US).contains(needle) || + matchInTag(needle); + } + } + + private boolean matchInTag(String needle) { + needle = needle.toLowerCase(Locale.US); + for (Tag tag : getTags()) { + if (tag.getName().toLowerCase(Locale.US).contains(needle)) { + return true; + } + } + return false; + } + + public ContentValues getContentValues() { + final ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid.toString()); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar); + values.put(LAST_PRESENCE, lastseen.presence); + values.put(LAST_TIME, lastseen.time); + values.put(GROUPS, groups.toString()); + return values; + } + + public int getSubscription() { + return this.subscription; + } + + public Account getAccount() { + return this.account; + } + + public void setAccount(Account account) { + this.account = account; + this.accountUuid = account.getUuid(); + } + + public Presences getPresences() { + return this.presences; + } + + public void setPresences(Presences pres) { + this.presences = pres; + } + + public void updatePresence(final String resource, final int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(final 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 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 void setSystemAccount(String account) { + this.systemAccount = account; + } + + public List getGroups() { + ArrayList groups = new ArrayList(); + for (int i = 0; i < this.groups.length(); ++i) { + try { + groups.add(this.groups.getString(i)); + } catch (final JSONException ignored) { + } + } + return groups; + } + + public ArrayList getOtrFingerprints() { + final ArrayList fingerprints = new ArrayList(); + try { + if (this.keys.has("otr_fingerprints")) { + final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); + for (int i = 0; i < prints.length(); ++i) { + final String print = prints.isNull(i) ? null : prints.getString(i); + if (print != null && !print.isEmpty()) { + fingerprints.add(prints.getString(i)); + } + } + } + } catch (final JSONException ignored) { + + } + return fingerprints; + } + + public boolean addOtrFingerprint(String print) { + if (getOtrFingerprints().contains(print)) { + return false; + } + 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); + return true; + } catch (final JSONException ignored) { + return false; + } + } + + 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 setPgpKeyId(long keyId) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (final JSONException ignored) { + + } + } + + 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) { + switch (subscription) { + case "to": + this.resetOption(Options.FROM); + this.setOption(Options.TO); + break; + case "from": + this.resetOption(Options.TO); + this.setOption(Options.FROM); + this.resetOption(Options.PREEMPTIVE_GRANT); + break; + case "both": + this.setOption(Options.TO); + this.setOption(Options.FROM); + this.resetOption(Options.PREEMPTIVE_GRANT); + break; + case "none": + this.resetOption(Options.FROM); + this.resetOption(Options.TO); + break; + } + } + + // 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 void parseGroupsFromElement(Element item) { + this.groups = new JSONArray(); + for (Element element : item.getChildren()) { + if (element.getName().equals("group") && element.getContent() != null) { + this.groups.put(element.getContent()); + } + } + } + + public Element asElement() { + final Element item = new Element("item"); + item.setAttribute("jid", this.jid.toString()); + if (this.serverName != null) { + item.setAttribute("name", this.serverName); + } + for (String group : getGroups()) { + item.addChild("group").setContent(group); + } + return item; + } + + @Override + public int compareTo(final ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } + + public Jid getServer() { + return getJid().toDomainJid(); + } + + 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); + } + + public String getShareableUri() { + if (getOtrFingerprints().size() >= 1) { + String otr = getOtrFingerprints().get(0); + return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr; + } else { + return "xmpp:" + getJid().toBareJid().toString(); + } + } + + @Override + public boolean isBlocked() { + return getAccount().isBlocked(this); + } + + @Override + public boolean isDomainBlocked() { + return getAccount().isBlocked(this.getJid().toDomainJid()); + } + + @Override + public Jid getBlockedJid() { + if (isDomainBlocked()) { + return getJid().toDomainJid(); + } else { + return getJid(); + } + } + + public static class Lastseen { + public long time; + public String presence; + + public Lastseen() { + this(null, 0); + } + + public Lastseen(final String presence, final long time) { + this.presence = presence; + this.time = time; + } + } + + public final 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; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java new file mode 100644 index 00000000..37cbff46 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java @@ -0,0 +1,770 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; + +import net.java.otr4j.OtrException; +import net.java.otr4j.crypto.OtrCryptoException; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionImpl; +import net.java.otr4j.session.SessionStatus; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class Conversation extends AbstractEntity implements Blockable { + 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"; + public static final String ATTRIBUTE_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted"; + + private String name; + private String contactUuid; + private String accountUuid; + private Jid contactJid; + private int status; + private long created; + private int mode; + + private JSONObject attributes = new JSONObject(); + + private Jid nextCounterpart; + + protected final ArrayList messages = new ArrayList<>(); + protected Account account = null; + + private transient SessionImpl otrSession; + + private transient String otrFingerprint = null; + private Smp mSmp = new Smp(); + + private String nextMessage; + + private transient MucOptions mucOptions = null; + + private byte[] symmetricKey; + + private Bookmark bookmark; + + private boolean messagesLeftOnServer = true; + private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE; + private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; + private String mLastReceivedOtrMessageId = null; + + public boolean hasMessagesLeftOnServer() { + return messagesLeftOnServer; + } + + public void setHasMessagesLeftOnServer(boolean value) { + this.messagesLeftOnServer = value; + } + + public Message findUnsentMessageWithUuid(String uuid) { + synchronized(this.messages) { + for (final Message message : this.messages) { + final int s = message.getStatus(); + if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { + return message; + } + } + } + return null; + } + + public void findWaitingMessages(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for(Message message : this.messages) { + if (message.getStatus() == Message.STATUS_WAITING) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public void findMessagesWithFiles(final OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) + && message.getEncryption() != Message.ENCRYPTION_PGP) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public Message findMessageWithFileAndUuid(final String uuid) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { + return message; + } + } + } + return null; + } + + public void clearMessages() { + synchronized (this.messages) { + this.messages.clear(); + } + } + + public boolean setIncomingChatState(ChatState state) { + if (this.mIncomingChatState == state) { + return false; + } + this.mIncomingChatState = state; + return true; + } + + public ChatState getIncomingChatState() { + return this.mIncomingChatState; + } + + public boolean setOutgoingChatState(ChatState state) { + if (mode == MODE_MULTI) { + return false; + } + if (this.mOutgoingChatState != state) { + this.mOutgoingChatState = state; + return true; + } else { + return false; + } + } + + public ChatState getOutgoingChatState() { + return this.mOutgoingChatState; + } + + public void trim() { + synchronized (this.messages) { + final int size = messages.size(); + final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; + if (size > maxsize) { + this.messages.subList(0, size - maxsize).clear(); + } + } + } + + public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) + && (message.getEncryption() == Message.ENCRYPTION_OTR)) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public void findUnsentTextMessages(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (message.getType() != Message.TYPE_IMAGE + && message.getStatus() == Message.STATUS_UNSEND) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public Message findSentMessageWithUuid(String uuid) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (uuid.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND && uuid + .equals(message.getRemoteMsgId()))) { + return message; + } + } + } + return null; + } + + public void populateWithMessages(final List messages) { + synchronized (this.messages) { + messages.clear(); + messages.addAll(this.messages); + } + } + + @Override + public boolean isBlocked() { + return getContact().isBlocked(); + } + + @Override + public boolean isDomainBlocked() { + return getContact().isDomainBlocked(); + } + + @Override + public Jid getBlockedJid() { + return getContact().getBlockedJid(); + } + + public String getLastReceivedOtrMessageId() { + return this.mLastReceivedOtrMessageId; + } + + public void setLastReceivedOtrMessageId(String id) { + this.mLastReceivedOtrMessageId = id; + } + + + public interface OnMessageFound { + public void onMessageFound(final Message message); + } + + public Conversation(final String name, final Account account, final Jid contactJid, + final int mode) { + this(java.util.UUID.randomUUID().toString(), name, null, account + .getUuid(), contactJid, System.currentTimeMillis(), + STATUS_AVAILABLE, mode, ""); + this.account = account; + } + + public Conversation(final String uuid, final String name, final String contactUuid, + final String accountUuid, final Jid contactJid, final long created, final int status, + final int mode, final 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 { + this.attributes = new JSONObject(attributes == null ? "" : attributes); + } catch (JSONException e) { + this.attributes = new JSONObject(); + } + } + + public boolean isRead() { + return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); + } + + public void markRead() { + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (messages.get(i).isRead()) { + break; + } + this.messages.get(i).markRead(); + } + } + + public Message getLatestMarkableMessage() { + 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); + } + } + } + return null; + } + + public Message getLatestMessage() { + if (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 String getName() { + if (getMode() == MODE_MULTI) { + if (getMucOptions().getSubject() != null) { + return getMucOptions().getSubject(); + } else if (bookmark != null && bookmark.getName() != null) { + return bookmark.getName(); + } else { + String generatedName = getMucOptions().createNameFromParticipants(); + if (generatedName != null) { + return generatedName; + } else { + return getJid().getLocalpart(); + } + } + } else { + return this.getContact().getDisplayName(); + } + } + + 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(final Account account) { + this.account = account; + } + + @Override + public Jid getJid() { + 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.toString()); + 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) { + Jid jid; + try { + jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true); + } catch (final InvalidJidException e) { + // Borked DB.. + jid = null; + } + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(NAME)), + cursor.getString(cursor.getColumnIndex(CONTACT)), + cursor.getString(cursor.getColumnIndex(ACCOUNT)), + jid, + 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(String presence, boolean sendStart) { + if (this.otrSession != null) { + return this.otrSession; + } else { + final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(), + presence, + "xmpp"); + this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine()); + try { + if (sendStart) { + this.otrSession.startSession(); + return this.otrSession; + } + return this.otrSession; + } catch (OtrException e) { + return null; + } + } + + } + + public SessionImpl getOtrSession() { + return this.otrSession; + } + + public void resetOtrSession() { + this.otrFingerprint = null; + this.otrSession = null; + this.mSmp.hint = null; + this.mSmp.secret = null; + this.mSmp.status = Smp.STATUS_NONE; + } + + public Smp smp() { + return mSmp; + } + + public 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 synchronized String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { + return null; + } + DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); + this.otrFingerprint = getAccount().getOtrEngine().getFingerprint(remotePubKey); + } catch (final OtrCryptoException | UnsupportedOperationException ignored) { + return null; + } + } + return this.otrFingerprint; + } + + public boolean verifyOtrFingerprint() { + final String fingerprint = getOtrFingerprint(); + if (fingerprint != null) { + getContact().addOtrFingerprint(fingerprint); + return true; + } else { + return false; + } + } + + public boolean isOtrFingerprintVerified() { + return getContact().getOtrFingerprints().contains(getOtrFingerprint()); + } + + 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(final Jid jid) { + this.contactJid = jid; + } + + public void setNextCounterpart(Jid jid) { + this.nextCounterpart = jid; + } + + public Jid getNextCounterpart() { + return this.nextCounterpart; + } + + 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 boolean smpRequested() { + return smp().status == Smp.STATUS_CONTACT_REQUESTED; + } + + 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) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).equals(message)) { + return true; + } + } + } + return false; + } + + public Message findSentMessageWithBody(String body) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + Message message = this.messages.get(i); + if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) { + return message; + } + } + return null; + } + } + + public boolean setLastMessageTransmitted(long value) { + long before = getLastMessageTransmitted(); + if (value - before > 1000) { + this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value)); + return true; + } else { + return false; + } + } + + public long getLastMessageTransmitted() { + long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0); + if (timestamp == 0) { + synchronized (this.messages) { + for(int i = this.messages.size() - 1; i >= 0; --i) { + Message message = this.messages.get(i); + if (message.getStatus() == Message.STATUS_RECEIVED) { + return message.getTimeSent(); + } + } + } + } + return timestamp; + } + + public void setMutedTill(long value) { + this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); + } + + public boolean isMuted() { + return System.currentTimeMillis() < 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); + } + } + + public void sort() { + synchronized (this.messages) { + Collections.sort(this.messages, new Comparator() { + @Override + public int compare(Message left, Message right) { + if (left.getTimeSent() < right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() > right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); + for(Message message : this.messages) { + message.untie(); + } + } + } + + public int unreadCount() { + synchronized (this.messages) { + int count = 0; + for(int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).isRead()) { + return count; + } + ++count; + } + return count; + } + } + + public class Smp { + public static final int STATUS_NONE = 0; + public static final int STATUS_CONTACT_REQUESTED = 1; + public static final int STATUS_WE_REQUESTED = 2; + public static final int STATUS_FAILED = 3; + public static final int STATUS_VERIFIED = 4; + + public String secret = null; + public String hint = null; + public int status = 0; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Downloadable.java b/src/main/java/de/thedevstack/conversationsplus/entities/Downloadable.java new file mode 100644 index 00000000..20169dc8 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Downloadable.java @@ -0,0 +1,28 @@ +package de.thedevstack.conversationsplus.entities; + +public interface Downloadable { + + public final String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"}; + 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 static final int STATUS_UPLOADING = 0x207; + + public boolean start(); + + public int getStatus(); + + public long getFileSize(); + + public int getProgress(); + + public String getMimeType(); + + public void cancel(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java new file mode 100644 index 00000000..7566c199 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java @@ -0,0 +1,172 @@ +package de.thedevstack.conversationsplus.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.net.URLConnection; +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 de.thedevstack.conversationsplus.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 String mime; + + 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 String getMimeType() { + String path = this.getAbsolutePath(); + try { + String mime = URLConnection.guessContentTypeFromName(path.replace("#","")); + if (mime != null) { + return mime; + } else if (mime == null && path.endsWith(".webp")) { + return "image/webp"; + } else { + return ""; + } + } catch (final StringIndexOutOfBoundsException e) { + return ""; + } + } + + 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/src/main/java/de/thedevstack/conversationsplus/entities/DownloadablePlaceholder.java b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadablePlaceholder.java new file mode 100644 index 00000000..741d2990 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadablePlaceholder.java @@ -0,0 +1,39 @@ +package de.thedevstack.conversationsplus.entities; + +public class DownloadablePlaceholder implements Downloadable { + + private int status; + + public DownloadablePlaceholder(int status) { + this.status = status; + } + @Override + public boolean start() { + return false; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public long getFileSize() { + return 0; + } + + @Override + public int getProgress() { + return 0; + } + + @Override + public String getMimeType() { + return ""; + } + + @Override + public void cancel() { + + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java new file mode 100644 index 00000000..825eb481 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java @@ -0,0 +1,33 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.List; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public interface ListItem extends Comparable { + public String getDisplayName(); + + public Jid getJid(); + + public List getTags(); + + public final class Tag { + private final String name; + private final int color; + + public Tag(final String name, final int color) { + this.name = name; + this.color = color; + } + + public int getColor() { + return this.color; + } + + public String getName() { + return this.name; + } + } + + public boolean match(final String needle); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java new file mode 100644 index 00000000..cd7556b3 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -0,0 +1,587 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.utils.GeoHelper; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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_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_FILE = 2; + public static final int TYPE_STATUS = 3; + public static final int TYPE_PRIVATE = 4; + + public static final String CONVERSATION = "conversationUuid"; + public static final String COUNTERPART = "counterpart"; + public static final String TRUE_COUNTERPART = "trueCounterpart"; + public static final String BODY = "body"; + public static final String TIME_SENT = "timeSent"; + public static final String ENCRYPTION = "encryption"; + public static final String STATUS = "status"; + public static final String TYPE = "type"; + public static final String REMOTE_MSG_ID = "remoteMsgId"; + public static final String SERVER_MSG_ID = "serverMsgId"; + public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + public static final String ME_COMMAND = "/me "; + + + public boolean markable = false; + protected String conversationUuid; + protected Jid counterpart; + protected Jid trueCounterpart; + protected String body; + protected String encryptedBody; + protected long timeSent; + protected int encryption; + protected int status; + protected int type; + protected String relativeFilePath; + protected boolean read = true; + protected String remoteMsgId = null; + protected String serverMsgId = null; + protected Conversation conversation = null; + protected Downloadable downloadable = null; + private Message mNextMessage = null; + private Message mPreviousMessage = null; + + private Message() { + + } + + public Message(Conversation conversation, String body, int encryption) { + this(conversation, body, encryption, STATUS_UNSEND); + } + + public Message(Conversation conversation, String body, int encryption, int status) { + this(java.util.UUID.randomUUID().toString(), + conversation.getUuid(), + conversation.getJid() == null ? null : conversation.getJid().toBareJid(), + null, + body, + System.currentTimeMillis(), + encryption, + status, + TYPE_TEXT, + null, + null, + null); + this.conversation = conversation; + } + + private Message(final String uuid, final String conversationUUid, final Jid counterpart, + final Jid trueCounterpart, final String body, final long timeSent, + final int encryption, final int status, final int type, final String remoteMsgId, + final String relativeFilePath, final String serverMsgId) { + 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; + this.relativeFilePath = relativeFilePath; + this.serverMsgId = serverMsgId; + } + + public static Message fromCursor(Cursor cursor) { + Jid jid; + try { + String value = cursor.getString(cursor.getColumnIndex(COUNTERPART)); + if (value != null) { + jid = Jid.fromString(value, true); + } else { + jid = null; + } + } catch (InvalidJidException e) { + jid = null; + } + Jid trueCounterpart; + try { + String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)); + if (value != null) { + trueCounterpart = Jid.fromString(value, true); + } else { + trueCounterpart = null; + } + } catch (InvalidJidException e) { + trueCounterpart = null; + } + return new Message(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + jid, + trueCounterpart, + 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)), + cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); + } + + public static Message createStatusMessage(Conversation conversation, String body) { + Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + message.setBody(body); + return message; + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(CONVERSATION, conversationUuid); + if (counterpart == null) { + values.putNull(COUNTERPART); + } else { + values.put(COUNTERPART, counterpart.toString()); + } + if (trueCounterpart == null) { + values.putNull(TRUE_COUNTERPART); + } else { + values.put(TRUE_COUNTERPART, trueCounterpart.toString()); + } + 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); + values.put(RELATIVE_FILE_PATH, relativeFilePath); + values.put(SERVER_MSG_ID,serverMsgId); + return values; + } + + public String getConversationUuid() { + return conversationUuid; + } + + public Conversation getConversation() { + return this.conversation; + } + + public void setConversation(Conversation conv) { + this.conversation = conv; + } + + public Jid getCounterpart() { + return counterpart; + } + + public void setCounterpart(final Jid counterpart) { + this.counterpart = 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 void setBody(String body) { + this.body = body; + } + + public long getTimeSent() { + return timeSent; + } + + public int getEncryption() { + return encryption; + } + + public void setEncryption(int encryption) { + this.encryption = encryption; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getRelativeFilePath() { + return this.relativeFilePath; + } + + public void setRelativeFilePath(String path) { + this.relativeFilePath = path; + } + + public String getRemoteMsgId() { + return this.remoteMsgId; + } + + public void setRemoteMsgId(String id) { + this.remoteMsgId = id; + } + + public String getServerMsgId() { + return this.serverMsgId; + } + + public void setServerMsgId(String id) { + this.serverMsgId = id; + } + + 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 String getEncryptedBody() { + return this.encryptedBody; + } + + public void setEncryptedBody(String body) { + this.encryptedBody = body; + } + + public int getType() { + return this.type; + } + + public void setType(int type) { + this.type = type; + } + + public void setTrueCounterpart(Jid trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + + public Downloadable getDownloadable() { + return this.downloadable; + } + + public void setDownloadable(Downloadable downloadable) { + this.downloadable = downloadable; + } + + public boolean equals(Message message) { + if (this.serverMsgId != null && message.getServerMsgId() != null) { + return this.serverMsgId.equals(message.getServerMsgId()); + } else if (this.body == null || this.counterpart == null) { + return false; + } else if (message.getRemoteMsgId() != null) { + return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) + && this.counterpart.equals(message.getCounterpart()) + && this.body.equals(message.getBody()); + } else { + return this.remoteMsgId == null + && this.counterpart.equals(message.getCounterpart()) + && this.body.equals(message.getBody()) + && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.PING_TIMEOUT * 500; + } + } + + public Message next() { + synchronized (this.conversation.messages) { + if (this.mNextMessage == null) { + int index = this.conversation.messages.indexOf(this); + if (index < 0 || index >= this.conversation.messages.size() - 1) { + this.mNextMessage = null; + } else { + this.mNextMessage = this.conversation.messages.get(index + 1); + } + } + return this.mNextMessage; + } + } + + public Message prev() { + synchronized (this.conversation.messages) { + if (this.mPreviousMessage == null) { + 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 mergeable(final Message message) { + return message != null && + (message.getType() == Message.TYPE_TEXT && + this.getDownloadable() == null && + message.getDownloadable() == null && + message.getEncryption() != Message.ENCRYPTION_PGP && + this.getType() == message.getType() && + //this.getStatus() == message.getStatus() && + isStatusMergeable(this.getStatus(),message.getStatus()) && + this.getEncryption() == message.getEncryption() && + this.getCounterpart() != null && + this.getCounterpart().equals(message.getCounterpart()) && + (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && + !GeoHelper.isGeoUri(message.getBody()) && + !GeoHelper.isGeoUri(this.body) && + !message.bodyContainsDownloadable() && + !this.bodyContainsDownloadable() && + !message.getBody().startsWith(ME_COMMAND) && + !this.getBody().startsWith(ME_COMMAND) + ); + } + + private static boolean isStatusMergeable(int a, int b) { + return a == b || ( + ( a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) + || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND) + || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED) + || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED) + ); + } + + public String getMergedBody() { + final Message next = this.next(); + if (this.mergeable(next)) { + return getBody() + '\n' + next.getMergedBody(); + } + return getBody(); + } + + public boolean hasMeCommand() { + return getMergedBody().startsWith(ME_COMMAND); + } + + public int getMergedStatus() { + final Message next = this.next(); + if (this.mergeable(next)) { + return next.getStatus(); + } + return getStatus(); + } + + public long getMergedTimeSent() { + Message next = this.next(); + if (this.mergeable(next)) { + return next.getMergedTimeSent(); + } else { + return getTimeSent(); + } + } + + public boolean wasMergedIntoPrevious() { + Message prev = this.prev(); + return prev != null && prev.mergeable(this); + } + + public boolean trusted() { + Contact contact = this.getContact(); + return (status > STATUS_RECEIVED || (contact != null && contact.trusted())); + } + + public boolean bodyContainsDownloadable() { + /** + * there are a few cases where spaces result in an unwanted behavior, e.g. + * "http://upload.mitsu-freunde-bw.de/uploads/2015/03/i43b4bpr8.png /abc.png" + * or more than one image link in one message. + */ + if (body.contains(" ")) { + return false; + } + try { + URL url = new URL(body); + if (!url.getProtocol().equalsIgnoreCase("http") + && !url.getProtocol().equalsIgnoreCase("https")) { + return false; + } + + String sUrlPath = url.getPath(); + if (sUrlPath == null || sUrlPath.isEmpty()) { + return false; + } + + int iSlashIndex = sUrlPath.lastIndexOf('/') + 1; + + String sLastUrlPath = sUrlPath.substring(iSlashIndex).toLowerCase(); + + String[] extensionParts = sLastUrlPath.split("\\."); + if (extensionParts.length == 2 + && Arrays.asList(Downloadable.VALID_IMAGE_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_IMAGE_EXTENSIONS).contains( + extensionParts[extensionParts.length - 2])) { + return true; + } else { + return false; + } + } catch (MalformedURLException e) { + return false; + } + } + + public ImageParams getImageParams() { + ImageParams params = getLegacyImageParams(); + if (params != null) { + return params; + } + 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]; + try { + params.url = new URL(parts[0]); + } catch (MalformedURLException e1) { + params.url = null; + } + } + } 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.url = new URL(parts[0]); + } catch (MalformedURLException e1) { + params.url = null; + } + 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 ImageParams getLegacyImageParams() { + ImageParams params = new ImageParams(); + if (body == null) { + return params; + } + String parts[] = body.split(","); + if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + return null; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + return null; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + return null; + } + return params; + } else { + return null; + } + } + + public void untie() { + this.mNextMessage = null; + this.mPreviousMessage = null; + } + + public boolean isFileOrImage() { + return type == TYPE_FILE || type == TYPE_IMAGE; + } + + public class ImageParams { + public URL url; + public long size = 0; + public int width = 0; + public int height = 0; + public String origin; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java b/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java new file mode 100644 index 00000000..860643e0 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java @@ -0,0 +1,530 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.PgpEngine; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket; + +import android.annotation.SuppressLint; + +@SuppressLint("DefaultLocale") +public class MucOptions { + + public enum Affiliation { + OWNER("owner", 4, R.string.owner), + ADMIN("admin", 3, R.string.admin), + MEMBER("member", 2, R.string.member), + OUTCAST("outcast", 0, R.string.outcast), + NONE("none", 1, R.string.no_affiliation); + + private Affiliation(String string, int rank, int resId) { + this.string = string; + this.resId = resId; + this.rank = rank; + } + + private String string; + private int resId; + private int rank; + + public int getResId() { + return resId; + } + + @Override + public String toString() { + return this.string; + } + + public boolean outranks(Affiliation affiliation) { + return rank > affiliation.rank; + } + + public boolean ranks(Affiliation affiliation) { + return rank >= affiliation.rank; + } + } + + public enum Role { + MODERATOR("moderator", R.string.moderator), + VISITOR("visitor", R.string.visitor), + PARTICIPANT("participant", R.string.participant), + NONE("none", R.string.no_role); + + private Role(String string, int resId) { + this.string = string; + this.resId = resId; + } + + private String string; + private int resId; + + public int getResId() { + return resId; + } + + @Override + public String toString() { + return this.string; + } + } + + public static final int ERROR_NO_ERROR = 0; + public static final int ERROR_NICK_IN_USE = 1; + public static final int ERROR_UNKNOWN = 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_ROOM_CONFIG_CHANGED = "104"; + public static final String STATUS_CODE_SELF_PRESENCE = "110"; + public static final String STATUS_CODE_BANNED = "301"; + public static final String STATUS_CODE_CHANGED_NICK = "303"; + public static final String STATUS_CODE_KICKED = "307"; + public static final String STATUS_CODE_LOST_MEMBERSHIP = "321"; + + private interface OnEventListener { + public void onSuccess(); + + public void onFailure(); + } + + public interface OnRenameListener extends OnEventListener { + + } + + public interface OnJoinListener extends OnEventListener { + + } + + public class User { + private Role role = Role.NONE; + private Affiliation affiliation = Affiliation.NONE; + private String name; + private Jid jid; + private long pgpKeyId = 0; + + public String getName() { + return name; + } + + public void setName(String user) { + this.name = user; + } + + public void setJid(Jid jid) { + this.jid = jid; + } + + public Jid getJid() { + return this.jid; + } + + public Role getRole() { + return this.role; + } + + public void setRole(String role) { + role = role.toLowerCase(); + switch (role) { + case "moderator": + this.role = Role.MODERATOR; + break; + case "participant": + this.role = Role.PARTICIPANT; + break; + case "visitor": + this.role = Role.VISITOR; + break; + default: + this.role = Role.NONE; + break; + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof User)) { + return false; + } else { + User o = (User) other; + return name != null && name.equals(o.name) + && jid != null && jid.equals(o.jid) + && affiliation == o.affiliation + && role == o.role; + } + } + + public Affiliation getAffiliation() { + return this.affiliation; + } + + public void setAffiliation(String affiliation) { + affiliation = affiliation.toLowerCase(); + switch (affiliation) { + case "admin": + this.affiliation = Affiliation.ADMIN; + break; + case "owner": + this.affiliation = Affiliation.OWNER; + break; + case "member": + this.affiliation = Affiliation.MEMBER; + break; + case "outcast": + this.affiliation = Affiliation.OUTCAST; + break; + default: + 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 List features = new ArrayList<>(); + private Conversation conversation; + private boolean isOnline = false; + private int error = ERROR_UNKNOWN; + private OnRenameListener onRenameListener = null; + private OnJoinListener onJoinListener = null; + private User self = new User(); + private String subject = null; + private String password = null; + private boolean mNickChangingInProgress = false; + + public MucOptions(Conversation conversation) { + this.account = conversation.getAccount(); + this.conversation = conversation; + } + + public void updateFeatures(ArrayList features) { + this.features.clear(); + this.features.addAll(features); + } + + public boolean hasFeature(String feature) { + return this.features.contains(feature); + } + + public boolean canInvite() { + return !membersOnly() || self.getAffiliation().ranks(Affiliation.ADMIN); + } + + public boolean membersOnly() { + return hasFeature("muc_membersonly"); + } + + public boolean nonanonymous() { + return hasFeature("muc_nonanonymous"); + } + + public boolean persistent() { + return hasFeature("muc_persistent"); + } + + 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) { + final Jid from = packet.getFrom(); + if (!from.isBareJid()) { + final String name = from.getResourcepart(); + final String type = packet.getAttribute("type"); + final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user"); + final List codes = getStatusCodes(x); + if (type == null) { + User user = new User(); + if (x != null) { + Element item = x.findChild("item"); + if (item != null && name != null) { + user.setName(name); + user.setAffiliation(item.getAttribute("affiliation")); + user.setRole(item.getAttribute("role")); + user.setJid(item.getAttributeAsJid("jid")); + if (codes.contains(STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(this.conversation.getJid())) { + this.isOnline = true; + this.error = ERROR_NO_ERROR; + self = user; + if (mNickChangingInProgress) { + onRenameListener.onSuccess(); + mNickChangingInProgress = false; + } else if (this.onJoinListener != null) { + this.onJoinListener.onSuccess(); + this.onJoinListener = null; + } + } else { + addUser(user); + } + if (pgp != null) { + Element signed = packet.findChild("x", "jabber:x:signed"); + if (signed != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + user.setPgpKeyId(pgp.fetchKeyId(account, msg, + signed.getContent())); + } + } + } + } + } else if (type.equals("unavailable")) { + if (codes.contains(STATUS_CODE_SELF_PRESENCE) || + packet.getFrom().equals(this.conversation.getJid())) { + if (codes.contains(STATUS_CODE_CHANGED_NICK)) { + this.mNickChangingInProgress = true; + } else if (codes.contains(STATUS_CODE_KICKED)) { + setError(KICKED_FROM_ROOM); + } else if (codes.contains(STATUS_CODE_BANNED)) { + setError(ERROR_BANNED); + } else if (codes.contains(STATUS_CODE_LOST_MEMBERSHIP)) { + setError(ERROR_MEMBERS_ONLY); + } else { + setError(ERROR_UNKNOWN); + } + } else { + deleteUser(name); + } + } else if (type.equals("error")) { + Element error = packet.findChild("error"); + if (error != null && error.hasChild("conflict")) { + if (isOnline) { + if (onRenameListener != null) { + onRenameListener.onFailure(); + } + } else { + setError(ERROR_NICK_IN_USE); + } + } else if (error != null && error.hasChild("not-authorized")) { + setError(ERROR_PASSWORD_REQUIRED); + } else if (error != null && error.hasChild("forbidden")) { + setError(ERROR_BANNED); + } else if (error != null && error.hasChild("registration-required")) { + setError(ERROR_MEMBERS_ONLY); + } else { + setError(ERROR_UNKNOWN); + } + } + } + } + + private void setError(int error) { + this.isOnline = false; + this.error = error; + if (onJoinListener != null) { + onJoinListener.onFailure(); + onJoinListener = null; + } + } + + private List getStatusCodes(Element x) { + List codes = new ArrayList<>(); + if (x != null) { + for (Element child : x.getChildren()) { + if (child.getName().equals("status")) { + String code = child.getAttribute("code"); + if (code != null) { + codes.add(code); + } + } + } + } + return codes; + } + + public List getUsers() { + return this.users; + } + + public String getProposedNick() { + if (conversation.getBookmark() != null + && conversation.getBookmark().getNick() != null + && !conversation.getBookmark().getNick().isEmpty()) { + return conversation.getBookmark().getNick(); + } else if (!conversation.getJid().isBareJid()) { + return conversation.getJid().getResourcepart(); + } else { + return account.getUsername(); + } + } + + public String getActualNick() { + if (this.self.getName() != null) { + return this.self.getName(); + } else { + return this.getProposedNick(); + } + } + + public boolean online() { + return this.isOnline; + } + + public int getError() { + return this.error; + } + + public void setOnRenameListener(OnRenameListener listener) { + this.onRenameListener = listener; + } + + public void setOnJoinListener(OnJoinListener listener) { + this.onJoinListener = listener; + } + + 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 String createNameFromParticipants() { + if (users.size() >= 2) { + List names = new ArrayList(); + for (User user : users) { + Contact contact = user.getContact(); + if (contact != null && !contact.getDisplayName().isEmpty()) { + names.add(contact.getDisplayName().split("\\s+")[0]); + } else { + names.add(user.getName()); + } + } + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < names.size(); ++i) { + builder.append(names.get(i)); + if (i != names.size() - 1) { + builder.append(", "); + } + } + return builder.toString(); + } else { + return null; + } + } + + 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 Jid createJoinJid(String nick) { + try { + return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick); + } catch (final InvalidJidException e) { + return null; + } + } + + public Jid 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; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java b/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java new file mode 100644 index 00000000..cb984648 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java @@ -0,0 +1,90 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; + +import de.thedevstack.conversationsplus.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) { + synchronized (this.presences) { + this.presences.put(resource, status); + } + } + + public void removePresence(String resource) { + synchronized (this.presences) { + this.presences.remove(resource); + } + } + + public void clearPresences() { + synchronized (this.presences) { + this.presences.clear(); + } + } + + public int getMostAvailableStatus() { + int status = OFFLINE; + synchronized (this.presences) { + 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() { + synchronized (this.presences) { + return presences.size(); + } + } + + public String[] asStringArray() { + synchronized (this.presences) { + final String[] presencesArray = new String[presences.size()]; + presences.keySet().toArray(presencesArray); + return presencesArray; + } + } + + public boolean has(String presence) { + synchronized (this.presences) { + return presences.containsKey(presence); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Roster.java b/src/main/java/de/thedevstack/conversationsplus/entities/Roster.java new file mode 100644 index 00000000..0c719ed9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Roster.java @@ -0,0 +1,91 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class Roster { + final Account account; + final HashMap contacts = new HashMap<>(); + private String version = null; + + public Roster(Account account) { + this.account = account; + } + + public Contact getContactFromRoster(Jid jid) { + if (jid == null) { + return null; + } + synchronized (this.contacts) { + Contact contact = contacts.get(jid.toBareJid().toString()); + if (contact != null && contact.showInRoster()) { + return contact; + } else { + return null; + } + } + } + + public Contact getContact(final Jid jid) { + synchronized (this.contacts) { + final Jid bareJid = jid.toBareJid(); + if (contacts.containsKey(bareJid.toString())) { + return contacts.get(bareJid.toString()); + } else { + Contact contact = new Contact(bareJid); + contact.setAccount(account); + contacts.put(bareJid.toString(), 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() { + synchronized (this.contacts) { + return new ArrayList<>(this.contacts.values()); + } + } + + public void initContact(final Contact contact) { + contact.setAccount(account); + contact.setOption(Contact.Options.IN_ROSTER); + synchronized (this.contacts) { + contacts.put(contact.getJid().toBareJid().toString(), contact); + } + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + + public Account getAccount() { + return this.account; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/generator/AbstractGenerator.java b/src/main/java/de/thedevstack/conversationsplus/generator/AbstractGenerator.java new file mode 100644 index 00000000..cb56228d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/generator/AbstractGenerator.java @@ -0,0 +1,91 @@ +package de.thedevstack.conversationsplus.generator; + +import android.util.Base64; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.PhoneHelper; + +public abstract class AbstractGenerator { + private 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", + "http://jabber.org/protocol/muc", + "jabber:x:conference", + "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify", + "urn:xmpp:ping", + "jabber:iq:version", + "http://jabber.org/protocol/chatstates"}; + private final String[] MESSAGE_CONFIRMATION_FEATURES = { + "urn:xmpp:chat-markers:0", + "urn:xmpp:receipts" + }; + private String mVersion = null; + public final String IDENTITY_NAME = "Conversations"; + public final String IDENTITY_TYPE = "phone"; + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractGenerator(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + protected String getIdentityVersion() { + if (mVersion == null) { + this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); + } + return this.mVersion; + } + + protected String getIdentityName() { + return IDENTITY_NAME + " " + getIdentityVersion(); + } + + public String getCapHash() { + StringBuilder s = new StringBuilder(); + s.append("client/" + IDENTITY_TYPE + "//" + getIdentityName() + "<"); + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + + for (String feature : getFeatures()) { + s.append(feature + "<"); + } + byte[] sha1 = md.digest(s.toString().getBytes()); + return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); + } + + public static String getTimestamp(long time) { + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + return DATE_FORMAT.format(time); + } + + public List getFeatures() { + ArrayList features = new ArrayList<>(); + features.addAll(Arrays.asList(FEATURES)); + if (Settings.CONFIRM_MESSAGE_RECEIVED) { + features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); + } + Collections.sort(features); + return features; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java b/src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java new file mode 100644 index 00000000..ccba1e1d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java @@ -0,0 +1,189 @@ +package de.thedevstack.conversationsplus.generator; + + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.services.MessageArchiveService; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.forms.Data; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.pep.Avatar; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class IqGenerator extends AbstractGenerator { + + public IqGenerator(final XmppConnectionService service) { + super(service); + } + + public IqPacket discoResponse(final IqPacket request) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); + packet.setId(request.getId()); + packet.setTo(request.getFrom()); + final Element query = packet.addChild("query", + "http://jabber.org/protocol/disco#info"); + query.setAttribute("node", request.query().getAttribute("node")); + final Element identity = query.addChild("identity"); + identity.setAttribute("category", "client"); + identity.setAttribute("type", IDENTITY_TYPE); + identity.setAttribute("name", getIdentityName()); + for (final String feature : getFeatures()) { + query.addChild("feature").setAttribute("var", feature); + } + return packet; + } + + public IqPacket versionResponse(final IqPacket request) { + final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + Element query = packet.query("jabber:iq:version"); + query.addChild("name").setContent(IDENTITY_NAME); + query.addChild("version").setContent(getIdentityVersion()); + return packet; + } + + protected IqPacket publish(final String node, final Element item) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + final Element publish = pubsub.addChild("publish"); + publish.setAttribute("node", node); + publish.addChild(item); + return packet; + } + + protected IqPacket retrieve(String node, Element item) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + final Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + final Element items = pubsub.addChild("items"); + items.setAttribute("node", node); + if (item != null) { + items.addChild(item); + } + return packet; + } + + public IqPacket publishAvatar(Avatar avatar) { + final Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + final Element data = item.addChild("data", "urn:xmpp:avatar:data"); + data.setContent(avatar.image); + return publish("urn:xmpp:avatar:data", item); + } + + public IqPacket publishAvatarMetadata(final Avatar avatar) { + final Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + final Element metadata = item + .addChild("metadata", "urn:xmpp:avatar:metadata"); + final 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(final Avatar avatar) { + final Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + packet.setTo(avatar.owner); + return packet; + } + + public IqPacket retrieveAvatarMetaData(final Jid to) { + final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + if (to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final Element query = packet.query("urn:xmpp:mam:0"); + query.setAttribute("queryid",mam.getQueryId()); + final Data data = new Data(); + data.setFormType("urn:xmpp:mam:0"); + if (mam.muc()) { + packet.setTo(mam.getWith()); + } else if (mam.getWith()!=null) { + data.put("with", mam.getWith().toString()); + } + data.put("start",getTimestamp(mam.getStart())); + data.put("end",getTimestamp(mam.getEnd())); + query.addChild(data); + if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { + query.addChild("set", "http://jabber.org/protocol/rsm").addChild("before").setContent(mam.getReference()); + } else if (mam.getReference() != null) { + query.addChild("set", "http://jabber.org/protocol/rsm").addChild("after").setContent(mam.getReference()); + } + return packet; + } + public IqPacket generateGetBlockList() { + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.addChild("blocklist", Xmlns.BLOCKING); + + return iq; + } + + public IqPacket generateSetBlockRequest(final Jid jid) { + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Element block = iq.addChild("block", Xmlns.BLOCKING); + block.addChild("item").setAttribute("jid", jid.toBareJid().toString()); + return iq; + } + + public IqPacket generateSetUnblockRequest(final Jid jid) { + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Element block = iq.addChild("unblock", Xmlns.BLOCKING); + block.addChild("item").setAttribute("jid", jid.toBareJid().toString()); + return iq; + } + + public IqPacket generateSetPassword(final Account account, final String newPassword) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(account.getServer()); + final Element query = packet.addChild("query", Xmlns.REGISTER); + final Jid jid = account.getJid(); + query.addChild("username").setContent(jid.getLocalpart()); + query.addChild("password").setContent(newPassword); + return packet; + } + + public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { + List jids = new ArrayList<>(); + jids.add(jid); + return changeAffiliation(conference,jids,affiliation); + } + + public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(conference.getJid().toBareJid()); + packet.setFrom(conference.getAccount().getJid()); + Element query = packet.query("http://jabber.org/protocol/muc#admin"); + for(Jid jid : jids) { + Element item = query.addChild("item"); + item.setAttribute("jid", jid.toString()); + item.setAttribute("affiliation", affiliation); + } + return packet; + } + + public IqPacket changeRole(Conversation conference, String nick, String role) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(conference.getJid().toBareJid()); + packet.setFrom(conference.getAccount().getJid()); + Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); + item.setAttribute("nick", nick); + item.setAttribute("role", role); + return packet; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java b/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java new file mode 100644 index 00000000..41b41882 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java @@ -0,0 +1,187 @@ +package de.thedevstack.conversationsplus.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 de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.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); + if (this.mXmppConnectionService.indicateReceived()) { + packet.addChild("request", "urn:xmpp:receipts"); + } + } else { + packet.setTo(message.getCounterpart().toBareJid()); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + } + packet.setFrom(account.getJid()); + 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())[0]); + 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 generateChatState(Conversation conversation) { + final Account account = conversation.getAccount(); + MessagePacket packet = new MessagePacket(); + packet.setTo(conversation.getJid().toBareJid()); + packet.setFrom(account.getJid()); + packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); + return packet; + } + + public MessagePacket confirm(final Account account, final Jid to, final String id) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(to); + packet.setFrom(account.getJid()); + 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.getJid().toBareJid()); + Element subjectChild = new Element("subject"); + subjectChild.setContent(subject); + packet.addChild(subjectChild); + packet.setFrom(conversation.getAccount().getJid().toBareJid()); + return packet; + } + + public MessagePacket directInvite(final Conversation conversation, final Jid contact) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(contact); + packet.setFrom(conversation.getAccount().getJid()); + Element x = packet.addChild("x", "jabber:x:conference"); + x.setAttribute("jid", conversation.getJid().toBareJid().toString()); + return packet; + } + + public MessagePacket invite(Conversation conversation, Jid contact) { + MessagePacket packet = new MessagePacket(); + packet.setTo(conversation.getJid().toBareJid()); + packet.setFrom(conversation.getAccount().getJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); + Element invite = new Element("invite"); + invite.setAttribute("to", contact.toBareJid().toString()); + 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.getJid()); + Element received = receivedPacket.addChild("received", namespace); + received.setAttribute("id", originalMessage.getId()); + return receivedPacket; + } + + public MessagePacket generateOtrError(Jid to, String id) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_ERROR); + packet.setAttribute("id",id); + packet.setTo(to); + Element error = packet.addChild("error"); + error.setAttribute("code","406"); + error.setAttribute("type","modify"); + error.addChild("not-acceptable","urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("text").setContent("unreadable OTR message received"); + return packet; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/generator/PresenceGenerator.java b/src/main/java/de/thedevstack/conversationsplus/generator/PresenceGenerator.java new file mode 100644 index 00000000..27fdd501 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/generator/PresenceGenerator.java @@ -0,0 +1,57 @@ +package de.thedevstack.conversationsplus.generator; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.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.setTo(contact.getJid()); + packet.setFrom(contact.getAccount().getJid().toBareJid()); + 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.setFrom(account.getJid()); + 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.im"); + cap.setAttribute("ver", capHash); + } + return packet; + } +} \ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnection.java new file mode 100644 index 00000000..96b64854 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnection.java @@ -0,0 +1,310 @@ +package de.thedevstack.conversationsplus.http; + +import android.content.Intent; +import android.net.Uri; +import android.os.SystemClock; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; + +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 java.util.Arrays; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.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; + private boolean acceptedAutomatically = false; + private int mProgress = 0; + private long mLastGuiRefresh = 0; + + 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()); + String[] parts = mUrl.getPath().toLowerCase().split("\\."); + String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null; + String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null; + if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) { + this.message.setEncryption(Message.ENCRYPTION_PGP); + } else if (message.getEncryption() != Message.ENCRYPTION_OTR) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + String extension; + if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) { + extension = secondToLast; + } else { + extension = lastPart; + } + message.setRelativeFilePath(message.getUuid()+"."+extension); + 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); + mXmppConnectionService.updateConversationUi(); + if (acceptedAutomatically) { + mXmppConnectionService.getNotificationService().push(message); + } + } + + private void changeStatus(int status) { + this.mStatus = status; + mXmppConnectionService.updateConversationUi(); + } + + private void setupTrustManager(final HttpsURLConnection connection, + final boolean interactive) { + final X509TrustManager trustManager; + final 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 { + final SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, new X509TrustManager[]{trustManager}, + mXmppConnectionService.getRNG()); + + final SSLSocketFactory sf = sc.getSocketFactory(); + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( + sf.getSupportedCipherSuites()); + if (cipherSuites.length > 0) { + sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); + + } + + connection.setSSLSocketFactory(sf); + connection.setHostnameVerifier(hostnameVerifier); + } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { + } + } + + 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); + HttpConnection.this.acceptedAutomatically = false; + HttpConnection.this.mXmppConnectionService.getNotificationService().push(message); + return; + } catch (IOException e) { + cancel(); + return; + } + file.setExpectedSize(size); + if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) { + HttpConnection.this.acceptedAutomatically = true; + new Thread(new FileDownloader(interactive)).start(); + } else { + changeStatus(STATUS_OFFER); + HttpConnection.this.acceptedAutomatically = false; + HttpConnection.this.mXmppConnectionService.getNotificationService().push(message); + } + } + + 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()); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream os = file.createOutputStream(); + if (os == null) { + throw new IOException(); + } + long transmitted = 0; + long expected = file.getExpectedSize(); + int count = -1; + byte[] buffer = new byte[1024]; + while ((count = is.read(buffer)) != -1) { + transmitted += count; + os.write(buffer, 0, count); + updateProgress((int) ((((double) transmitted) / expected) * 100)); + } + os.flush(); + os.close(); + is.close(); + } + + private void updateImageBounds() { + message.setType(Message.TYPE_IMAGE); + mXmppConnectionService.getFileBackend().updateFileParams(message,mUrl); + mXmppConnectionService.updateMessage(message); + } + + } + + public void updateProgress(int i) { + this.mProgress = i; + if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { + this.mLastGuiRefresh = SystemClock.elapsedRealtime(); + mXmppConnectionService.updateConversationUi(); + } + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } + + @Override + public int getProgress() { + return this.mProgress; + } + + @Override + public String getMimeType() { + return ""; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java new file mode 100644 index 00000000..b41f0793 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java @@ -0,0 +1,28 @@ +package de.thedevstack.conversationsplus.http; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.AbstractConnectionManager; +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/parser/AbstractParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/AbstractParser.java new file mode 100644 index 00000000..5ab0faa6 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/parser/AbstractParser.java @@ -0,0 +1,104 @@ +package de.thedevstack.conversationsplus.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public abstract class AbstractParser { + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractParser(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + /** + * Gets the timestamp from the 'delay' element. + * Refer to XEP-0203: Delayed Delivery for details. @link{http://xmpp.org/extensions/xep-0203.html} + * @param packet the element to find the child element 'delay' in. + * @return the time in milli seconds of the attribute 'stamp' of the + * element 'delay'. In case there is no 'delay' element or no 'stamp' + * attribute or the current time is less than the value of the 'stamp' + * attribute the current time is returned. + */ + protected long getTimestamp(Element packet) { + long now = System.currentTimeMillis(); + Element delay = packet.findChild("delay"); + if (delay == null) { + return now; + } + String stamp = delay.getAttribute("stamp"); + if (stamp == null) { + return now; + } + /*long time = parseTimestamp(stamp).getTime(); + return now < time ? now : time;*/ + try { + long time = parseTimestamp(stamp).getTime(); + return now < time ? now : time; + } catch (ParseException e) { + return now; + } + } + + /** + * Parses the timestamp according to XEP-0082: XMPP Date and Time Profiles. + * @link{http://xmpp.org/extensions/xep-0082.html} + * + * @param timestamp the timestamp to parse + * @return Date + * @throws ParseException + */ + public static Date parseTimestamp(String timestamp) throws ParseException { + /*try { + Log.d("TIMESTAMP", timestamp); + return DatatypeFactory.newInstance().newXMLGregorianCalendar(timestamp).toGregorianCalendar().getTime(); + } catch (DatatypeConfigurationException e) { + Log.d("TIMESTAMP", e.getMessage()); + return new Date(); + }*/ + timestamp = timestamp.replace("Z", "+0000"); + SimpleDateFormat dateFormat; + timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length()); + dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US); + return dateFormat.parse(timestamp); + } + + protected void updateLastseen(final Element packet, final Account account, + final boolean presenceOverwrite) { + final Jid from = packet.getAttributeAsJid("from"); + updateLastseen(packet, account, from, presenceOverwrite); + } + + protected void updateLastseen(final Element packet, final Account account, final Jid from, + final boolean presenceOverwrite) { + final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart(); + final Contact contact = account.getRoster().getContact(from); + final long timestamp = getTimestamp(packet); + if (timestamp >= contact.lastseen.time) { + contact.lastseen.time = timestamp; + if (!presence.isEmpty() && 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/src/main/java/de/thedevstack/conversationsplus/parser/IqParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/IqParser.java new file mode 100644 index 00000000..f76dbf03 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/parser/IqParser.java @@ -0,0 +1,158 @@ +package de.thedevstack.conversationsplus.parser; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collection; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class IqParser extends AbstractParser implements OnIqPacketReceived { + + public IqParser(final XmppConnectionService service) { + super(service); + } + + private void rosterItems(final Account account, final Element query) { + final String version = query.getAttribute("ver"); + if (version != null) { + account.getRoster().setVersion(version); + } + for (final Element item : query.getChildren()) { + if (item.getName().equals("item")) { + final Jid jid = item.getAttributeAsJid("jid"); + if (jid == null) { + continue; + } + final String name = item.getAttribute("name"); + final String subscription = item.getAttribute("subscription"); + final Contact contact = account.getRoster().getContact(jid); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + contact.parseGroupsFromElement(item); + } + 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.getAvatarService().clear(contact); + } + } + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + } + + public String avatarData(final IqPacket packet) { + final Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub == null) { + return null; + } + final Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return super.avatarData(items); + } + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) { + final Element query = packet.findChild("query"); + // If this is in response to a query for the whole roster: + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.getRoster().markAllAsNotInRoster(); + } + this.rosterItems(account, query); + } else if ((packet.hasChild("block", Xmlns.BLOCKING) || packet.hasChild("blocklist", Xmlns.BLOCKING)) && + packet.fromServer(account)) { + // Block list or block push. + Log.d(Config.LOGTAG, "Received blocklist update from server"); + final Element blocklist = packet.findChild("blocklist", Xmlns.BLOCKING); + final Element block = packet.findChild("block", Xmlns.BLOCKING); + final Collection items = blocklist != null ? blocklist.getChildren() : + (block != null ? block.getChildren() : null); + // If this is a response to a blocklist query, clear the block list and replace with the new one. + // Otherwise, just update the existing blocklist. + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.clearBlocklist(); + account.getXmppConnection().getFeatures().setBlockListRequested(true); + } + if (items != null) { + final Collection jids = new ArrayList<>(items.size()); + // Create a collection of Jids from the packet + for (final Element item : items) { + if (item.getName().equals("item")) { + final Jid jid = item.getAttributeAsJid("jid"); + if (jid != null) { + jids.add(jid); + } + } + } + account.getBlocklist().addAll(jids); + } + // Update the UI + mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + } else if (packet.hasChild("unblock", Xmlns.BLOCKING) && + packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { + Log.d(Config.LOGTAG, "Received unblock update from server"); + final Collection items = packet.findChild("unblock", Xmlns.BLOCKING).getChildren(); + if (items.size() == 0) { + // No children to unblock == unblock all + account.getBlocklist().clear(); + } else { + final Collection jids = new ArrayList<>(items.size()); + for (final Element item : items) { + if (item.getName().equals("item")) { + final Jid jid = item.getAttributeAsJid("jid"); + if (jid != null) { + jids.add(jid); + } + } + } + account.getBlocklist().removeAll(jids); + } + mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + } 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")) { + final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet); + mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("query","jabber:iq:version")) { + final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); + mXmppConnectionService.sendIqPacket(account,response,null); + } else if (packet.hasChild("ping", "urn:xmpp:ping")) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } else { + if ((packet.getType() == IqPacket.TYPE.GET) + || (packet.getType() == IqPacket.TYPE.SET)) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + final 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/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java new file mode 100644 index 00000000..67ff94d2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java @@ -0,0 +1,653 @@ +package de.thedevstack.conversationsplus.parser; + +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.http.HttpConnectionManager; +import de.thedevstack.conversationsplus.services.MessageArchiveService; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnMessagePacketReceived; +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.pep.Avatar; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; + +public class MessageParser extends AbstractParser implements + OnMessagePacketReceived { + public MessageParser(XmppConnectionService service) { + super(service); + } + + private boolean extractChatState(Conversation conversation, final Element element) { + ChatState state = ChatState.parse(element); + if (state != null && conversation != null) { + final Account account = conversation.getAccount(); + Jid from = element.getAttributeAsJid("from"); + if (from != null && from.toBareJid().equals(account.getJid().toBareJid())) { + conversation.setOutgoingChatState(state); + return false; + } else { + return conversation.setIncomingChatState(state); + } + } + return false; + } + + private Message parseChat(MessagePacket packet, Account account) { + final Jid jid = packet.getFrom(); + if (jid == null) { + return null; + } + Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid.toBareJid(), false); + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, + pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED); + } else { + finishedMessage = new Message(conversation, + packet.getBody(), Message.ENCRYPTION_NONE, + Message.STATUS_RECEIVED); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + if (conversation.getMode() == Conversation.MODE_MULTI + && !jid.isBareJid()) { + final Jid trueCounterpart = conversation.getMucOptions() + .getTrueCounterpart(jid.getResourcepart()); + if (trueCounterpart != null) { + updateLastseen(packet, account, trueCounterpart, false); + } + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setTrueCounterpart(trueCounterpart); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + } else { + updateLastseen(packet, account, true); + } + finishedMessage.setCounterpart(jid); + finishedMessage.setTime(getTimestamp(packet)); + extractChatState(conversation,packet); + return finishedMessage; + } + + private Message parseOtrChat(MessagePacket packet, Account account) { + final Jid to = packet.getTo(); + final Jid from = packet.getFrom(); + if (to == null || from == null) { + return null; + } + boolean properlyAddressed = !to.isBareJid() || account.countPresences() == 1; + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, from.toBareJid(), false); + String presence; + if (from.isBareJid()) { + presence = ""; + } else { + presence = from.getResourcepart(); + } + extractChatState(conversation, packet); + updateLastseen(packet, account, true); + String body = packet.getBody(); + if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { + conversation.endOtrIfNeeded(); + } + if (!conversation.hasValidOtrSession()) { + if (properlyAddressed) { + conversation.startOtrSession(presence,false); + } else { + return null; + } + } else { + String foreignPresence = conversation.getOtrSession() + .getSessionID().getUserID(); + if (!foreignPresence.equals(presence)) { + conversation.endOtrIfNeeded(); + if (properlyAddressed) { + conversation.startOtrSession(presence, false); + } else { + return null; + } + } + } + try { + conversation.setLastReceivedOtrMessageId(packet.getId()); + Session otrSession = conversation.getOtrSession(); + SessionStatus before = otrSession.getSessionStatus(); + body = otrSession.transformReceiving(body); + SessionStatus after = otrSession.getSessionStatus(); + if ((before != after) && (after == SessionStatus.ENCRYPTED)) { + conversation.setNextEncryption(Message.ENCRYPTION_OTR); + mXmppConnectionService.onOtrSessionEstablished(conversation); + } else if ((before != after) && (after == SessionStatus.FINISHED)) { + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + 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, body, Message.ENCRYPTION_OTR, + Message.STATUS_RECEIVED); + finishedMessage.setTime(getTimestamp(packet)); + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + finishedMessage.setCounterpart(from); + conversation.setLastReceivedOtrMessageId(null); + return finishedMessage; + } catch (Exception e) { + conversation.resetOtrSession(); + return null; + } + } + + private Message parseGroupchat(MessagePacket packet, Account account) { + int status; + final Jid from = packet.getFrom(); + if (from == null) { + return null; + } + if (mXmppConnectionService.find(account.pendingConferenceLeaves, + account, from.toBareJid()) != null) { + return null; + } + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, from.toBareJid(), true); + final Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(from.getResourcepart()); + if (trueCounterpart != null) { + updateLastseen(packet, account, trueCounterpart, false); + } + if (packet.hasChild("subject")) { + conversation.setHasMessagesLeftOnServer(true); + conversation.getMucOptions().setSubject(packet.findChild("subject").getContent()); + mXmppConnectionService.updateConversationUi(); + return null; + } + + final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user"); + if (from.isBareJid() && (x == null || !x.hasChild("status"))) { + return null; + } else if (from.isBareJid() && x.hasChild("status")) { + for(Element child : x.getChildren()) { + if (child.getName().equals("status")) { + String code = child.getAttribute("code"); + if (code.contains(MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED)) { + mXmppConnectionService.fetchConferenceConfiguration(conversation); + } + } + } + return null; + } + + if (from.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { + if (mXmppConnectionService.markMessage(conversation, + packet.getId(), Message.STATUS_SEND_RECEIVED)) { + return null; + } else if (packet.getId() == null) { + Message message = conversation.findSentMessageWithBody(packet.getBody()); + if (message != null) { + mXmppConnectionService.markMessage(message,Message.STATUS_SEND_RECEIVED); + return null; + } else { + status = Message.STATUS_SEND; + } + } else { + status = Message.STATUS_SEND; + } + } else { + status = Message.STATUS_RECEIVED; + } + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody == null) { + finishedMessage = new Message(conversation, + packet.getBody(), Message.ENCRYPTION_NONE, status); + } else { + finishedMessage = new Message(conversation, pgpBody, + Message.ENCRYPTION_PGP, status); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + finishedMessage.setCounterpart(from); + if (status == Message.STATUS_RECEIVED) { + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(from.getResourcepart())); + } + if (packet.hasChild("delay") + && conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + private Message parseCarbonMessage(final MessagePacket packet, final Account account) { + int status; + final Jid 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")) { + final Jid to = message.getAttributeAsJid("to"); + if (to != null) { + final Conversation conversation = mXmppConnectionService.find( + mXmppConnectionService.getConversations(), account, + to.toBareJid()); + if (conversation != null) { + mXmppConnectionService.markRead(conversation); + } + } + } + return null; + } + if (status == Message.STATUS_RECEIVED) { + fullJid = message.getAttributeAsJid("from"); + if (fullJid == null) { + return null; + } else { + updateLastseen(message, account, true); + } + } else { + fullJid = message.getAttributeAsJid("to"); + if (fullJid == null) { + return null; + } + } + if (message.hasChild("x","http://jabber.org/protocol/muc#user") + && "chat".equals(message.getAttribute("type"))) { + return null; + } + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fullJid.toBareJid(), false); + String pgpBody = getPgpBody(message); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, pgpBody, + Message.ENCRYPTION_PGP, status); + } else { + String body = message.findChild("body").getContent(); + finishedMessage = new Message(conversation, body, + Message.ENCRYPTION_NONE, status); + } + extractChatState(conversation,message); + finishedMessage.setTime(getTimestamp(message)); + finishedMessage.setRemoteMsgId(message.getAttribute("id")); + finishedMessage.markable = isMarkable(message); + finishedMessage.setCounterpart(fullJid); + if (conversation.getMode() == Conversation.MODE_MULTI + && !fullJid.isBareJid()) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(fullJid.getResourcepart())); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + } + return finishedMessage; + } + + private Message parseMamMessage(MessagePacket packet, final Account account) { + final Element result = packet.findChild("result","urn:xmpp:mam:0"); + if (result == null ) { + return null; + } + final MessageArchiveService.Query query = this.mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid")); + if (query!=null) { + query.incrementTotalCount(); + } + final Element forwarded = result.findChild("forwarded","urn:xmpp:forward:0"); + if (forwarded == null) { + return null; + } + final Element message = forwarded.findChild("message"); + if (message == null) { + return null; + } + final Element body = message.findChild("body"); + if (body == null || message.hasChild("private","urn:xmpp:carbons:2") || message.hasChild("no-copy","urn:xmpp:hints")) { + return null; + } + int encryption; + String content = getPgpBody(message); + if (content != null) { + encryption = Message.ENCRYPTION_PGP; + } else { + encryption = Message.ENCRYPTION_NONE; + content = body.getContent(); + } + if (content == null) { + return null; + } + final long timestamp = getTimestamp(forwarded); + final Jid to = message.getAttributeAsJid("to"); + final Jid from = message.getAttributeAsJid("from"); + Jid counterpart; + int status; + Conversation conversation; + if (from!=null && to != null && from.toBareJid().equals(account.getJid().toBareJid())) { + status = Message.STATUS_SEND; + conversation = this.mXmppConnectionService.findOrCreateConversation(account,to.toBareJid(),false,query); + counterpart = to; + } else if (from !=null && to != null) { + status = Message.STATUS_RECEIVED; + conversation = this.mXmppConnectionService.findOrCreateConversation(account,from.toBareJid(),false,query); + counterpart = from; + } else { + return null; + } + Message finishedMessage = new Message(conversation,content,encryption,status); + finishedMessage.setTime(timestamp); + finishedMessage.setCounterpart(counterpart); + finishedMessage.setRemoteMsgId(message.getAttribute("id")); + finishedMessage.setServerMsgId(result.getAttribute("id")); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + if (query!=null) { + query.incrementMessageCount(); + } + return finishedMessage; + } + + private void parseError(final MessagePacket packet, final Account account) { + final Jid from = packet.getFrom(); + mXmppConnectionService.markMessage(account, from.toBareJid(), + packet.getId(), Message.STATUS_SEND_FAILED); + } + + private void parseNonMessage(Element packet, Account account) { + final Jid from = packet.getAttributeAsJid("from"); + if (extractChatState(from == null ? null : mXmppConnectionService.find(account,from), packet)) { + mXmppConnectionService.updateConversationUi(); + } + Element invite = extractInvite(packet); + if (invite != null) { + Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, from, true); + if (!conversation.getMucOptions().online()) { + Element password = invite.findChild("password"); + conversation.getMucOptions().setPassword(password == null ? null : password.getContent()); + mXmppConnectionService.databaseBackend.updateConversation(conversation); + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + } + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, 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); + final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), id, Message.STATUS_SEND_DISPLAYED); + Message message = displayedMessage == null ? null :displayedMessage.prev(); + while (message != null + && message.getStatus() == Message.STATUS_SEND_RECEIVED + && message.getTimeSent() < displayedMessage.getTimeSent()) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED); + message = message.prev(); + } + } 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.toBareJid(), + 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.toBareJid(), + id, Message.STATUS_SEND_RECEIVED); + } + } + + private Element extractInvite(Element message) { + Element x = message.findChild("x","http://jabber.org/protocol/muc#user"); + if (x == null) { + x = message.findChild("x","jabber:x:conference"); + } + if (x != null && x.hasChild("invite")) { + return x; + } else { + return null; + } + } + + private void parseEvent(final Element event, final Jid from, final Account account) { + Element items = event.findChild("items"); + if (items == null) { + return; + } + String node = items.getAttribute("node"); + if (node == null) { + return; + } + 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().toBareJid().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()); + mXmppConnectionService.getAvatarService().clear(account); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateAccountUi(); + } + } + } + } + } + + 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; + 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") && extractInvite(packet) == null) { + 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(); + mXmppConnectionService.markRead(message.getConversation()); + } else { + message.markUnread(); + } + } + } else if (packet.hasChild("result","urn:xmpp:mam:0")) { + message = parseMamMessage(packet, account); + if (message != null) { + Conversation conversation = message.getConversation(); + conversation.add(message); + mXmppConnectionService.databaseBackend.createMessage(message); + } + return; + } else if (packet.hasChild("fin","urn:xmpp:mam:0")) { + Element fin = packet.findChild("fin","urn:xmpp:mam:0"); + mXmppConnectionService.getMessageArchiveService().processFin(fin); + } 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(); + } else { + mXmppConnectionService.markRead(message.getConversation()); + account.activateGracePeriod(); + } + } + } 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 ((Settings.CONFIRM_MESSAGE_RECEIVED) + && ((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 (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().advancedStreamFeaturesLoaded()) { + if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { + mXmppConnectionService.updateConversation(conversation); + } + } + + if (message.getStatus() == Message.STATUS_RECEIVED + && conversation.getOtrSession() != null + && !conversation.getOtrSession().getSessionID().getUserID() + .equals(message.getCounterpart().getResourcepart())) { + conversation.endOtrIfNeeded(); + } + + if (packet.getType() != MessagePacket.TYPE_ERROR) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || mXmppConnectionService.saveEncryptedMessages()) { + mXmppConnectionService.databaseBackend.createMessage(message); + } + } + final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); + if (message.trusted() + && Settings.DOWNLOAD_IMAGE_LINKS + && mXmppConnectionService.isDownloadAllowedInConnection() + && message.bodyContainsDownloadable() + && manager.getAutoAcceptFileSize() > 0) { + manager.createNewConnection(message); + } else if (!message.isRead()) { + 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/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java new file mode 100644 index 00000000..cfa09fd7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java @@ -0,0 +1,118 @@ +package de.thedevstack.conversationsplus.parser; + +import java.util.ArrayList; + +import de.thedevstack.conversationsplus.crypto.PgpEngine; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.entities.Presences; +import de.thedevstack.conversationsplus.generator.PresenceGenerator; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnPresencePacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.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(); + final Conversation conversation = packet.getFrom() == null ? null : mXmppConnectionService.find(account, packet.getFrom().toBareJid()); + if (conversation != null) { + final MucOptions mucOptions = conversation.getMucOptions(); + boolean before = mucOptions.online(); + int count = mucOptions.getUsers().size(); + final ArrayList tileUserBefore = new ArrayList<>(mucOptions.getUsers().subList(0,Math.min(mucOptions.getUsers().size(),5))); + mucOptions.processPacket(packet, mPgpEngine); + final ArrayList tileUserAfter = new ArrayList<>(mucOptions.getUsers().subList(0,Math.min(mucOptions.getUsers().size(),5))); + if (!tileUserAfter.equals(tileUserBefore)) { + mXmppConnectionService.getAvatarService().clear(conversation); + } + if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUsers().size())) { + mXmppConnectionService.updateConversationUi(); + } else if (mucOptions.online()) { + mXmppConnectionService.updateMucRosterUi(); + } + } + } + + public void parseContactPresence(PresencePacket packet, Account account) { + PresenceGenerator mPresenceGenerator = mXmppConnectionService + .getPresenceGenerator(); + if (packet.getFrom() == null) { + return; + } + final Jid from = packet.getFrom(); + String type = packet.getAttribute("type"); + Contact contact = account.getRoster().getContact(packet.getFrom()); + if (type == null) { + String presence; + if (!from.isBareJid()) { + presence = from.getResourcepart(); + } 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, false); + mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online); + } else if (type.equals("unavailable")) { + if (from.isBareJid()) { + contact.clearPresences(); + } else { + contact.removePresence(from.getResourcepart()); + } + 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/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java new file mode 100644 index 00000000..bee19e4f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java @@ -0,0 +1,400 @@ +package de.thedevstack.conversationsplus.persistance; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.Roster; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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 = 13; + + 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, " + + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + + Contact.GROUPS + " 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.RELATIVE_FILE_PATH + " TEXT, " + + Message.SERVER_MSG_ID + " TEXT, " + + 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"); + } + if (oldVersion < 9 && newVersion >= 9) { + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_TIME + " NUMBER"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_PRESENCE + " TEXT"); + } + if (oldVersion < 10 && newVersion >= 10) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.RELATIVE_FILE_PATH + " TEXT"); + } + if (oldVersion < 11 && newVersion >= 11) { + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.GROUPS + " TEXT"); + db.execSQL("delete from "+Contact.TABLENAME); + db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL"); + } + if (oldVersion < 12 && newVersion >= 12) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.SERVER_MSG_ID + " TEXT"); + } + if (oldVersion < 13 && newVersion >= 13) { + db.execSQL("delete from "+Contact.TABLENAME); + db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL"); + } + } + + 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(); + int count = cursor.getInt(0); + cursor.close(); + return count; + } + + 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)); + } + cursor.close(); + 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()); + } + cursor.close(); + return list; + } + + public Conversation findConversation(final Account account, final Jid contactJid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { account.getUuid(), + contactJid.toBareJid().toString() + "/%", + contactJid.toBareJid().toString() + }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID + + " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null); + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + Conversation conversation = Conversation.fromCursor(cursor); + cursor.close(); + return conversation; + } + + public void updateConversation(final Conversation conversation) { + final SQLiteDatabase db = this.getWritableDatabase(); + final 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 + } catch (RuntimeException 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(final Roster roster) { + final Account account = roster.getAccount(); + final 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().toString() }; + 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(); + Conversation conversation = Conversation.fromCursor(cursor); + cursor.close(); + return conversation; + } + + 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(); + Message message = Message.fromCursor(cursor); + cursor.close(); + return message; + } + + 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(); + Account account = Account.fromCursor(cursor); + cursor.close(); + return account; + } + + public List getImageMessages(Conversation conversation) { + ArrayList list = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + String[] selectionArgs = { conversation.getUuid(), String.valueOf(Message.TYPE_IMAGE) }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? AND "+Message.TYPE+"=?", selectionArgs, null, null,null); + if (cursor.getCount() > 0) { + cursor.moveToLast(); + do { + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); + } while (cursor.moveToPrevious()); + } + cursor.close(); + return list; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java new file mode 100644 index 00000000..e3bf58c7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java @@ -0,0 +1,525 @@ +package de.thedevstack.conversationsplus.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.net.URL; +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.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Base64OutputStream; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.ExifHelper; +import de.thedevstack.conversationsplus.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) { + String path = message.getRelativeFilePath(); + String extension; + if (path != null && !path.isEmpty()) { + String[] parts = path.split("\\."); + extension = "."+parts[parts.length - 1]; + } else { + if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_TEXT) { + extension = ".webp"; + } else { + extension = ""; + } + path = message.getUuid()+extension; + } + final boolean encrypted = !decrypted + && (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); + if (encrypted) { + return new DownloadableFile(getConversationsFileDirectory()+message.getUuid()+extension+".pgp"); + } else { + if (path.startsWith("/")) { + return new DownloadableFile(path); + } else { + if (message.getType() == Message.TYPE_FILE) { + return new DownloadableFile(getConversationsFileDirectory() + path); + } else { + return new DownloadableFile(getConversationsImageDirectory()+path); + } + } + } + } + + public static String getConversationsFileDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/"; + } + + public static String getConversationsImageDirectory() { + 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 String getOriginalPath(Uri uri) { + String path = null; + if (uri.getScheme().equals("file")) { + return uri.getPath(); + } else if (uri.toString().startsWith("content://media/")) { + String[] projection = {MediaStore.MediaColumns.DATA}; + Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri, + projection, null, null, null); + if (metaCursor != null) { + try { + if (metaCursor.moveToFirst()) { + path = metaCursor.getString(0); + } + } finally { + metaCursor.close(); + } + } + } + return path; + } + + public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { + try { + Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); + String mime = mXmppConnectionService.getContentResolver().getType(uri); + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); + message.setRelativeFilePath(message.getUuid() + "." + extension); + DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream os = new FileOutputStream(file); + InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri); + byte[] buffer = new byte[1024]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + os.flush(); + os.close(); + is.close(); + Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message)); + return file; + } catch (FileNotFoundException e) { + throw new FileCopyException(R.string.error_file_not_found); + } catch (IOException e) { + throw new FileCopyException(R.string.error_io_exception); + } + } + + public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) + throws FileCopyException { + return this.copyImageToPrivateStorage(message, image, 0); + } + + private DownloadableFile copyImageToPrivateStorage(Message message, + Uri image, int sampleSize) throws FileCopyException { + 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 FileCopyException(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 FileCopyException(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 FileCopyException(R.string.error_file_not_found); + } catch (IOException e) { + throw new FileCopyException(R.string.error_io_exception); + } catch (SecurityException e) { + throw new FileCopyException( + R.string.error_security_exception_during_image_copy); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize <= 3) { + return copyImageToPrivateStorage(message, image, sampleSize); + } else { + throw new FileCopyException(R.string.error_out_of_memory); + } + } + } + + private int getRotation(Uri image) { + try { + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + return ExifHelper.getOrientation(is); + } catch (FileNotFoundException e) { + return 0; + } + } + + 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 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) { + File file; + if (isAvatarCached(avatar)) { + file = new File(getAvatarPath(avatar.getFilename())); + } else { + String filename = getAvatarPath(avatar.getFilename()); + 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(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + } 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; + } + } + avatar.size = file.length(); + return true; + } + + 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) { + if (image == null) { + return null; + } + 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) { + if (image == null) { + return null; + } + 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 void updateFileParams(Message message) { + updateFileParams(message,null); + } + + public void updateFileParams(Message message, URL url) { + DownloadableFile file = getFile(message); + if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + if (url == null) { + message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); + } else { + message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); + } + } else { + message.setBody(Long.toString(file.getSize())); + } + + } + + public class FileCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public FileCopyException(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/src/main/java/de/thedevstack/conversationsplus/persistance/OnPhoneContactsMerged.java b/src/main/java/de/thedevstack/conversationsplus/persistance/OnPhoneContactsMerged.java new file mode 100644 index 00000000..cfbb6ef1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/OnPhoneContactsMerged.java @@ -0,0 +1,5 @@ +package de.thedevstack.conversationsplus.persistance; + +public interface OnPhoneContactsMerged { + public void phoneContactsMerged(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java new file mode 100644 index 00000000..0dedc01d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java @@ -0,0 +1,23 @@ +package de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java new file mode 100644 index 00000000..7321fc08 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java @@ -0,0 +1,295 @@ +package de.thedevstack.conversationsplus.services; + +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 java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Bookmark; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.ListItem; +import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.utils.UIHelper; + +public class AvatarService { + + private static final int FG_COLOR = 0xFFFAFAFA; + private static final int TRANSPARENT = 0x00000000; + private static final int PLACEHOLDER_COLOR = 0xFF202020; + + 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"; + + final private ArrayList sizes = new ArrayList<>(); + + protected XmppConnectionService mXmppConnectionService = null; + + public AvatarService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { + final String KEY = key(contact, size); + Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (avatar != null || cachedOnly) { + return avatar; + } + if (contact.getProfilePhoto() != null) { + avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); + } + if (avatar == null && contact.getAvatar() != null) { + avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); + } + if (avatar == null) { + avatar = get(contact.getDisplayName(), size, cachedOnly); + } + this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Contact contact) { + synchronized (this.sizes) { + 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().toBareJid() + "_" + + contact.getJid() + "_" + String.valueOf(size); + } + + public Bitmap get(ListItem item, int size) { + return get(item,size,false); + } + + public Bitmap get(ListItem item, int size, boolean cachedOnly) { + if (item instanceof Contact) { + return get((Contact) item, size,cachedOnly); + } else if (item instanceof Bookmark) { + Bookmark bookmark = (Bookmark) item; + if (bookmark.getConversation() != null) { + return get(bookmark.getConversation(), size, cachedOnly); + } else { + return get(bookmark.getDisplayName(), size, cachedOnly); + } + } else { + return get(item.getDisplayName(), size, cachedOnly); + } + } + + public Bitmap get(Conversation conversation, int size) { + return get(conversation,size,false); + } + + public Bitmap get(Conversation conversation, int size, boolean cachedOnly) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + return get(conversation.getContact(), size, cachedOnly); + } else { + return get(conversation.getMucOptions(), size, cachedOnly); + } + } + + public void clear(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + clear(conversation.getContact()); + } else { + clear(conversation.getMucOptions()); + } + } + + private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) { + final String KEY = key(mucOptions, size); + Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null || cachedOnly) { + return bitmap; + } + final List users = new ArrayList<>(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(); + final String letter = name.isEmpty() ? "X" : name.substring(0,1); + final int color = UIHelper.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", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1, + size, size); + } + this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + return bitmap; + } + + public void clear(MucOptions options) { + synchronized (this.sizes) { + 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; + } + avatar = mXmppConnectionService.getFileBackend().getAvatar( + account.getAvatar(), size); + if (avatar == null) { + avatar = get(account.getJid().toBareJid().toString(), size,false); + } + mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Account account) { + synchronized (this.sizes) { + 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) { + return get(name,size,false); + } + + public Bitmap get(final String name, final int size, boolean cachedOnly) { + final String KEY = key(name, size); + Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null || cachedOnly) { + return bitmap; + } + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + final String trimmedName = name.trim(); + final String letter = trimmedName.isEmpty() ? "X" : trimmedName.substring(0,1); + final int color = UIHelper.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.getProfilePhoto() != null) { + uri = Uri.parse(contact.getProfilePhoto()); + } else if (contact.getAvatar() != null) { + uri = mXmppConnectionService.getFileBackend().getAvatarUri( + contact.getAvatar()); + } + if (uri != null) { + Bitmap bitmap = mXmppConnectionService.getFileBackend() + .cropCenter(uri, bottom - top, right - left); + if (bitmap != null) { + drawTile(canvas, bitmap, left, top, right, bottom); + return; + } + } + } + String name = contact != null ? contact.getDisplayName() : user.getName(); + final String letter = name.isEmpty() ? "X" : name.substring(0,1); + final int color = UIHelper.getColorForName(name); + 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); + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java b/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java new file mode 100644 index 00000000..4367dd1b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java @@ -0,0 +1,24 @@ +package de.thedevstack.conversationsplus.services; + +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java new file mode 100644 index 00000000..8a6d1929 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java @@ -0,0 +1,371 @@ +package de.thedevstack.conversationsplus.services; + +import android.util.Log; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.generator.AbstractGenerator; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnAdvancedStreamFeaturesLoaded; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { + + private final XmppConnectionService mXmppConnectionService; + + private final HashSet queries = new HashSet(); + private final ArrayList pendingQueries = new ArrayList(); + + public enum PagingOrder { + NORMAL, + REVERSE + }; + + public MessageArchiveService(final XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void catchup(final Account account) { + long startCatchup = getLastMessageTransmitted(account); + long endCatchup = account.getXmppConnection().getLastSessionEstablished(); + if (startCatchup == 0) { + return; + } else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) { + startCatchup = endCatchup - Config.MAM_MAX_CATCHUP; + List conversations = mXmppConnectionService.getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) { + this.query(conversation,startCatchup); + } + } + } + final Query query = new Query(account, startCatchup, endCatchup); + this.queries.add(query); + this.execute(query); + } + + private long getLastMessageTransmitted(final Account account) { + long timestamp = 0; + for(final Conversation conversation : mXmppConnectionService.getConversations()) { + if (conversation.getAccount() == account) { + long tmp = conversation.getLastMessageTransmitted(); + if (tmp > timestamp) { + timestamp = tmp; + } + } + } + return timestamp; + } + + public Query query(final Conversation conversation) { + return query(conversation,conversation.getAccount().getXmppConnection().getLastSessionEstablished()); + } + + public Query query(final Conversation conversation, long end) { + return this.query(conversation,conversation.getLastMessageTransmitted(),end); + } + + public Query query(Conversation conversation, long start, long end) { + synchronized (this.queries) { + if (start > end) { + return null; + } + final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); + this.queries.add(query); + this.execute(query); + return query; + } + } + + public void executePendingQueries(final Account account) { + List pending = new ArrayList<>(); + synchronized(this.pendingQueries) { + for(Iterator iterator = this.pendingQueries.iterator(); iterator.hasNext();) { + Query query = iterator.next(); + if (query.getAccount() == account) { + pending.add(query); + iterator.remove(); + } + } + } + for(Query query : pending) { + this.execute(query); + } + } + + private void execute(final Query query) { + final Account account= query.getAccount(); + if (account.getStatus() == Account.State.ONLINE) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString()); + IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); + this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); + finalizeQuery(query); + } + } + }); + } else { + synchronized (this.pendingQueries) { + this.pendingQueries.add(query); + } + } + } + + private void finalizeQuery(Query query) { + synchronized (this.queries) { + this.queries.remove(query); + } + final Conversation conversation = query.getConversation(); + if (conversation != null) { + conversation.sort(); + if (conversation.setLastMessageTransmitted(query.getEnd())) { + this.mXmppConnectionService.databaseBackend.updateConversation(conversation); + } + conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); + if (query.hasCallback()) { + query.callback(); + } else { + this.mXmppConnectionService.updateConversationUi(); + } + } else { + for(Conversation tmp : this.mXmppConnectionService.getConversations()) { + if (tmp.getAccount() == query.getAccount()) { + tmp.sort(); + if (tmp.setLastMessageTransmitted(query.getEnd())) { + this.mXmppConnectionService.databaseBackend.updateConversation(tmp); + } + } + } + } + } + + public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { + synchronized (this.queries) { + for(Query query : queries) { + if (query.conversation == conversation) { + if (!query.hasCallback() && callback != null) { + query.setCallback(callback); + } + return true; + } + } + return false; + } + } + + public void processFin(Element fin) { + if (fin == null) { + return; + } + Query query = findQuery(fin.getAttribute("queryid")); + if (query == null) { + return; + } + boolean complete = fin.getAttributeAsBoolean("complete"); + Element set = fin.findChild("set","http://jabber.org/protocol/rsm"); + Element last = set == null ? null : set.findChild("last"); + Element first = set == null ? null : set.findChild("first"); + Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first; + boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES; + if (complete || relevant == null || abort) { + this.finalizeQuery(query); + Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages"); + } else { + final Query nextQuery; + if (query.getPagingOrder() == PagingOrder.NORMAL) { + nextQuery = query.next(last == null ? null : last.getContent()); + } else { + nextQuery = query.prev(first == null ? null : first.getContent()); + } + this.execute(nextQuery); + this.finalizeQuery(query); + synchronized (this.queries) { + this.queries.remove(query); + this.queries.add(nextQuery); + } + } + } + + public Query findQuery(String id) { + if (id == null) { + return null; + } + synchronized (this.queries) { + for(Query query : this.queries) { + if (query.getQueryId().equals(id)) { + return query; + } + } + return null; + } + } + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) { + this.catchup(account); + } + } + + public class Query { + private int totalCount = 0; + private int messageCount = 0; + private long start; + private long end; + private String queryId; + private String reference = null; + private Account account; + private Conversation conversation; + private PagingOrder pagingOrder = PagingOrder.NORMAL; + private XmppConnectionService.OnMoreMessagesLoaded callback = null; + + + public Query(Conversation conversation, long start, long end) { + this(conversation.getAccount(), start, end); + this.conversation = conversation; + } + + public Query(Conversation conversation, long start, long end, PagingOrder order) { + this(conversation,start,end); + this.pagingOrder = order; + } + + public Query(Account account, long start, long end) { + this.account = account; + this.start = start; + this.end = end; + this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + } + + private Query page(String reference) { + Query query = new Query(this.account,this.start,this.end); + query.reference = reference; + query.conversation = conversation; + query.totalCount = totalCount; + query.callback = callback; + return query; + } + + public Query next(String reference) { + Query query = page(reference); + query.pagingOrder = PagingOrder.NORMAL; + return query; + } + + public Query prev(String reference) { + Query query = page(reference); + query.pagingOrder = PagingOrder.REVERSE; + return query; + } + + public String getReference() { + return reference; + } + + public PagingOrder getPagingOrder() { + return this.pagingOrder; + } + + public String getQueryId() { + return queryId; + } + + public Jid getWith() { + return conversation == null ? null : conversation.getJid().toBareJid(); + } + + public boolean muc() { + return conversation != null && conversation.getMode() == Conversation.MODE_MULTI; + } + + public long getStart() { + return start; + } + + public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) { + this.callback = callback; + } + + public void callback() { + if (this.callback != null) { + this.callback.onMoreMessagesLoaded(messageCount,conversation); + if (messageCount == 0) { + this.callback.informUser(R.string.no_more_history_on_server); + } + } + } + + public long getEnd() { + return end; + } + + public Conversation getConversation() { + return conversation; + } + + public Account getAccount() { + return this.account; + } + + public void incrementTotalCount() { + this.totalCount++; + } + + public void incrementMessageCount() { + this.messageCount++; + } + + public int getTotalCount() { + return this.totalCount; + } + + public int getMessageCount() { + return this.messageCount; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (this.muc()) { + builder.append("to="+this.getWith().toString()); + } else { + builder.append("with="); + if (this.getWith() == null) { + builder.append("*"); + } else { + builder.append(getWith().toString()); + } + } + builder.append(", start="); + builder.append(AbstractGenerator.getTimestamp(this.start)); + builder.append(", end="); + builder.append(AbstractGenerator.getTimestamp(this.end)); + if (this.reference!=null) { + if (this.pagingOrder == PagingOrder.NORMAL) { + builder.append(", after="); + } else { + builder.append(", before="); + } + builder.append(this.reference); + } + return builder.toString(); + } + + public boolean hasCallback() { + return this.callback != null; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java new file mode 100644 index 00000000..50cc793b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java @@ -0,0 +1,557 @@ +package de.thedevstack.conversationsplus.services; + +import android.annotation.SuppressLint; +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.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.os.SystemClock; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.BigPictureStyle; +import android.support.v4.app.NotificationCompat.Builder; +import android.support.v4.app.TaskStackBuilder; +import android.text.Html; +import android.util.DisplayMetrics; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.ui.ConversationActivity; +import de.thedevstack.conversationsplus.ui.ManageAccountActivity; +import de.thedevstack.conversationsplus.ui.TimePreference; +import de.thedevstack.conversationsplus.utils.GeoHelper; +import de.thedevstack.conversationsplus.utils.UIHelper; + +public class NotificationService { + + private final XmppConnectionService mXmppConnectionService; + + private final LinkedHashMap> notifications = new LinkedHashMap<>(); + + public static final int NOTIFICATION_ID = 0x2342; + public static final int FOREGROUND_NOTIFICATION_ID = 0x8899; + public static final int ERROR_NOTIFICATION_ID = 0x5678; + + private Conversation mOpenConversation; + private boolean mIsInForeground; + private long mLastNotification; + + public NotificationService(final XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public boolean notify(final Message message) { + return (message.getStatus() == Message.STATUS_RECEIVED) + && notificationsEnabled() + && !message.getConversation().isMuted() + && (message.getConversation().getMode() == Conversation.MODE_SINGLE + || conferenceNotificationsEnabled() + || wasHighlightedOrPrivate(message) + ); + } + + public void notifyPebble(final Message message) { + final Intent i = new Intent("com.getpebble.action.SEND_NOTIFICATION"); + + final Conversation conversation = message.getConversation(); + final JSONObject jsonData = new JSONObject(new HashMap(2) {{ + put("title", conversation.getName()); + put("body", message.getBody()); + }}); + final String notificationData = new JSONArray().put(jsonData).toString(); + + i.putExtra("messageType", "PEBBLE_ALERT"); + i.putExtra("sender", "Conversations"); /* XXX: Shouldn't be hardcoded, e.g., AbstractGenerator.APP_NAME); */ + i.putExtra("notificationData", notificationData); + + mXmppConnectionService.sendBroadcast(i); + } + + + public boolean notificationsEnabled() { + return mXmppConnectionService.getPreferences().getBoolean("show_notification", true); + } + + public boolean isQuietHours() { + if (!mXmppConnectionService.getPreferences().getBoolean("enable_quiet_hours", false)) { + return false; + } + final long startTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; + final long endTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; + final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY; + + if (endTime < startTime) { + return nowTime > startTime || nowTime < endTime; + } else { + return nowTime > startTime && nowTime < endTime; + } + } + + public boolean conferenceNotificationsEnabled() { + return mXmppConnectionService.getPreferences().getBoolean("always_notify_in_conference", false); + } + + @SuppressLint("NewApi") + @SuppressWarnings("deprecation") + private boolean isInteractive() { + final PowerManager pm = (PowerManager) mXmppConnectionService + .getSystemService(Context.POWER_SERVICE); + + final boolean isScreenOn; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + isScreenOn = pm.isScreenOn(); + } else { + isScreenOn = pm.isInteractive(); + } + + return isScreenOn; + } + + public void push(final Message message) { + if (!notify(message)) { + return; + } + + final boolean isScreenOn = isInteractive(); + + if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) { + return; + } + + synchronized (notifications) { + final String conversationUuid = message.getConversationUuid(); + if (notifications.containsKey(conversationUuid)) { + notifications.get(conversationUuid).add(message); + } else { + final ArrayList mList = new ArrayList<>(); + mList.add(message); + notifications.put(conversationUuid, mList); + } + final Account account = message.getConversation().getAccount(); + final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) + && !account.inGracePeriod() + && !this.inMiniGracePeriod(account); + updateNotification(doNotify); + if (doNotify) { + notifyPebble(message); + } + } + } + + public void clear() { + synchronized (notifications) { + notifications.clear(); + updateNotification(false); + } + } + + public void clear(final Conversation conversation) { + synchronized (notifications) { + notifications.remove(conversation.getUuid()); + updateNotification(false); + } + } + + private void setNotificationColor(final Builder mBuilder) { + mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary)); + } + + private void updateNotification(final boolean notify) { + final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService + .getSystemService(Context.NOTIFICATION_SERVICE); + final SharedPreferences preferences = mXmppConnectionService.getPreferences(); + + final String ringtone = preferences.getString("notification_ringtone", null); + final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true); + + if (notifications.size() == 0) { + notificationManager.cancel(NOTIFICATION_ID); + } else { + if (notify) { + this.markLastNotification(); + } + final Builder mBuilder; + if (notifications.size() == 1) { + mBuilder = buildSingleConversations(notify); + } else { + mBuilder = buildMultipleConversation(); + } + if (notify && !isQuietHours()) { + if (vibrate) { + final int dat = 70; + final long[] pattern = {0, 3 * dat, dat, dat}; + mBuilder.setVibrate(pattern); + } + if (ringtone != null) { + mBuilder.setSound(Uri.parse(ringtone)); + } + } + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mBuilder.setCategory(Notification.CATEGORY_MESSAGE); + } + setNotificationColor(mBuilder); + mBuilder.setDefaults(0); + mBuilder.setSmallIcon(R.drawable.ic_notification); + mBuilder.setDeleteIntent(createDeleteIntent()); + mBuilder.setLights(Settings.LED_COLOR, 2000, 4000); + final Notification notification = mBuilder.build(); + notificationManager.notify(NOTIFICATION_ID, notification); + } + } + + private Builder buildMultipleConversation() { + final Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); + style.setBigContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + final StringBuilder names = new StringBuilder(); + Conversation conversation = null; + for (final ArrayList messages : notifications.values()) { + if (messages.size() > 0) { + conversation = messages.get(0).getConversation(); + final String name = conversation.getName(); + style.addLine(Html.fromHtml("" + name + " " + + UIHelper.getMessagePreview(mXmppConnectionService,messages.get(0)).first)); + 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)); + } + return mBuilder; + } + + private Builder buildSingleConversations(final boolean notify) { + final Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + final ArrayList messages = notifications.values().iterator().next(); + if (messages.size() >= 1) { + final Conversation conversation = messages.get(0).getConversation(); + mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() + .get(conversation, getPixel(64))); + mBuilder.setContentTitle(conversation.getName()); + Message message; + if ((message = getImage(messages)) != null) { + modifyForImage(mBuilder, message, messages, notify); + } else { + modifyForTextOnly(mBuilder, messages, notify); + } + if ((message = getFirstDownloadableMessage(messages)) != null) { + mBuilder.addAction( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? + R.drawable.ic_file_download_white_24dp : R.drawable.ic_action_download, + mXmppConnectionService.getResources().getString(R.string.download_x_file, + UIHelper.getFileDescriptionString(mXmppConnectionService, message)), + createDownloadIntent(message) + ); + } + if ((message = getFirstLocationMessage(messages)) != null) { + mBuilder.addAction(R.drawable.ic_room_white_24dp, + mXmppConnectionService.getString(R.string.show_location), + createShowLocationIntent(message)); + } + mBuilder.setContentIntent(createContentIntent(conversation)); + } + return mBuilder; + } + + private void modifyForImage(final Builder builder, final Message message, + final ArrayList messages, final boolean notify) { + try { + final Bitmap bitmap = mXmppConnectionService.getFileBackend() + .getThumbnail(message, getPixel(288), false); + final ArrayList tmp = new ArrayList<>(); + for (final Message msg : messages) { + if (msg.getType() == Message.TYPE_TEXT + && msg.getDownloadable() == null) { + tmp.add(msg); + } + } + final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); + bigPictureStyle.bigPicture(bitmap); + if (tmp.size() > 0) { + bigPictureStyle.setSummaryText(getMergedBodies(tmp)); + builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService,tmp.get(0)).first); + } else { + builder.setContentText(mXmppConnectionService.getString( + R.string.received_x_file, + UIHelper.getFileDescriptionString(mXmppConnectionService,message))); + } + builder.setStyle(bigPictureStyle); + } catch (final FileNotFoundException e) { + modifyForTextOnly(builder, messages, notify); + } + } + + private void modifyForTextOnly(final Builder builder, + final ArrayList messages, final boolean notify) { + builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); + builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(0)).first); + if (notify) { + builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(messages.size() - 1)).first); + } + } + + private Message getImage(final Iterable messages) { + for (final Message message : messages) { + if (message.getType() == Message.TYPE_IMAGE + && message.getDownloadable() == null + && message.getEncryption() != Message.ENCRYPTION_PGP) { + return message; + } + } + return null; + } + + private Message getFirstDownloadableMessage(final Iterable messages) { + for (final Message message : messages) { + if ((message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) && + message.getDownloadable() != null) { + return message; + } + } + return null; + } + + private Message getFirstLocationMessage(final Iterable messages) { + for(final Message message : messages) { + if (GeoHelper.isGeoUri(message.getBody())) { + return message; + } + } + return null; + } + + private CharSequence getMergedBodies(final ArrayList messages) { + final StringBuilder text = new StringBuilder(); + for (int i = 0; i < messages.size(); ++i) { + text.append(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(i)).first); + if (i != messages.size() - 1) { + text.append("\n"); + } + } + return text.toString(); + } + + private PendingIntent createShowLocationIntent(final Message message) { + Iterable intents = GeoHelper.createGeoIntentsFromMessage(message); + for(Intent intent : intents) { + if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) { + return PendingIntent.getActivity(mXmppConnectionService,18,intent,PendingIntent.FLAG_UPDATE_CURRENT); + } + } + return createOpenConversationsIntent(); + } + + private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) { + final TaskStackBuilder stackBuilder = TaskStackBuilder + .create(mXmppConnectionService); + stackBuilder.addParentStack(ConversationActivity.class); + + final Intent viewConversationIntent = new Intent(mXmppConnectionService, + ConversationActivity.class); + if (downloadMessageUuid != null) { + viewConversationIntent.setAction(ConversationActivity.ACTION_DOWNLOAD); + } else { + viewConversationIntent.setAction(Intent.ACTION_VIEW); + } + if (conversationUuid != null) { + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid); + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + } + if (downloadMessageUuid != null) { + viewConversationIntent.putExtra(ConversationActivity.MESSAGE, downloadMessageUuid); + } + + stackBuilder.addNextIntent(viewConversationIntent); + + return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private PendingIntent createDownloadIntent(final Message message) { + return createContentIntent(message.getConversationUuid(), message.getUuid()); + } + + private PendingIntent createContentIntent(final Conversation conversation) { + return createContentIntent(conversation.getUuid(), null); + } + + private PendingIntent createDeleteIntent() { + final Intent intent = new Intent(mXmppConnectionService, + XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); + return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + } + + private PendingIntent createDisableForeground() { + final Intent intent = new Intent(mXmppConnectionService, + XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND); + return PendingIntent.getService(mXmppConnectionService, 34, intent, 0); + } + + private PendingIntent createTryAgainIntent() { + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN); + return PendingIntent.getService(mXmppConnectionService, 45, intent, 0); + } + + private PendingIntent createDisableAccountIntent(final Account account) { + final Intent intent = new Intent(mXmppConnectionService,XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_DISABLE_ACCOUNT); + intent.putExtra("account",account.getJid().toBareJid().toString()); + return PendingIntent.getService(mXmppConnectionService,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); + } + + private boolean wasHighlightedOrPrivate(final Message message) { + final String nick = message.getConversation().getMucOptions().getActualNick(); + final Pattern highlight = generateNickHighlightPattern(nick); + if (message.getBody() == null || nick == null) { + return false; + } + final Matcher m = highlight.matcher(message.getBody()); + return (m.find() || message.getType() == Message.TYPE_PRIVATE); + } + + private static Pattern generateNickHighlightPattern(final 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(final Conversation conversation) { + this.mOpenConversation = conversation; + } + + public void setIsInForeground(final boolean foreground) { + this.mIsInForeground = foreground; + } + + private int getPixel(final int dp) { + final DisplayMetrics metrics = mXmppConnectionService.getResources() + .getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + + private void markLastNotification() { + this.mLastNotification = SystemClock.elapsedRealtime(); + } + + private boolean inMiniGracePeriod(final Account account) { + final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD + : Config.MINI_GRACE_PERIOD * 2; + return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); + } + + public Notification createForegroundNotification() { + final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service)); + mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations)); + mBuilder.setContentIntent(createOpenConversationsIntent()); + mBuilder.setWhen(0); + mBuilder.setPriority(NotificationCompat.PRIORITY_MIN); + final int cancelIcon; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mBuilder.setCategory(Notification.CATEGORY_SERVICE); + mBuilder.setSmallIcon(R.drawable.ic_import_export_white_48dp); + cancelIcon = R.drawable.ic_cancel_white_24dp; + } else { + mBuilder.setSmallIcon(R.drawable.ic_stat_communication_import_export); + cancelIcon = R.drawable.ic_action_cancel; + } + mBuilder.addAction(cancelIcon, + mXmppConnectionService.getString(R.string.disable_foreground_service), + createDisableForeground()); + setNotificationColor(mBuilder); + return mBuilder.build(); + } + + private PendingIntent createOpenConversationsIntent() { + return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService,ConversationActivity.class),0); + } + + public void updateErrorNotification() { + final NotificationManager mNotificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); + final List errors = new ArrayList<>(); + for (final Account account : mXmppConnectionService.getAccounts()) { + if (account.hasErrorStatus()) { + errors.add(account); + } + } + final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + if (errors.size() == 0) { + mNotificationManager.cancel(ERROR_NOTIFICATION_ID); + return; + } else if (errors.size() == 1) { + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account)); + mBuilder.setContentText(errors.get(0).getJid().toBareJid().toString()); + } else { + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts)); + mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix)); + } + mBuilder.addAction(R.drawable.ic_autorenew_white_24dp, + mXmppConnectionService.getString(R.string.try_again), + createTryAgainIntent()); + if (errors.size() == 1) { + mBuilder.addAction(R.drawable.ic_block_white_24dp, + mXmppConnectionService.getString(R.string.disable_account), + createDisableAccountIntent(errors.get(0))); + } + mBuilder.setOngoing(true); + //mBuilder.setLights(0xffffffff, 2000, 4000); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mBuilder.setSmallIcon(R.drawable.ic_warning_white_36dp); + } else { + mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning); + } + final TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService); + stackBuilder.addParentStack(ConversationActivity.class); + + final Intent manageAccountsIntent = new Intent(mXmppConnectionService,ManageAccountActivity.class); + stackBuilder.addNextIntent(manageAccountsIntent); + + final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT); + + mBuilder.setContentIntent(resultPendingIntent); + mNotificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build()); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java new file mode 100644 index 00000000..3a78cac6 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java @@ -0,0 +1,2434 @@ +package de.thedevstack.conversationsplus.services; + +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.Looper; +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; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionStatus; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.duenndns.ssl.MemorizingTrustManager; +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.PgpEngine; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Blockable; +import de.thedevstack.conversationsplus.entities.Bookmark; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.DownloadablePlaceholder; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.entities.MucOptions.OnRenameListener; +import de.thedevstack.conversationsplus.entities.Presences; +import de.thedevstack.conversationsplus.generator.IqGenerator; +import de.thedevstack.conversationsplus.generator.MessageGenerator; +import de.thedevstack.conversationsplus.generator.PresenceGenerator; +import de.thedevstack.conversationsplus.http.HttpConnectionManager; +import de.thedevstack.conversationsplus.parser.IqParser; +import de.thedevstack.conversationsplus.parser.MessageParser; +import de.thedevstack.conversationsplus.parser.PresenceParser; +import de.thedevstack.conversationsplus.persistance.DatabaseBackend; +import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.ui.UiCallback; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.ExceptionHelper; +import de.thedevstack.conversationsplus.utils.OnPhoneContactsLoadedListener; +import de.thedevstack.conversationsplus.utils.PRNGFixes; +import de.thedevstack.conversationsplus.utils.PhoneHelper; +import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnBindListener; +import de.thedevstack.conversationsplus.xmpp.OnContactStatusChanged; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.OnMessageAcknowledged; +import de.thedevstack.conversationsplus.xmpp.OnMessagePacketReceived; +import de.thedevstack.conversationsplus.xmpp.OnPresencePacketReceived; +import de.thedevstack.conversationsplus.xmpp.OnStatusChanged; +import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; +import de.thedevstack.conversationsplus.xmpp.XmppConnection; +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; +import de.thedevstack.conversationsplus.xmpp.forms.Data; +import de.thedevstack.conversationsplus.xmpp.forms.Field; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.jingle.JingleConnectionManager; +import de.thedevstack.conversationsplus.xmpp.jingle.OnJinglePacketReceived; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; +import de.thedevstack.conversationsplus.xmpp.pep.Avatar; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket; + +public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener { + + public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground"; + private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static final String ACTION_TRY_AGAIN = "try_again"; + public static final String ACTION_DISABLE_ACCOUNT = "disable_account"; + 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 final IBinder mBinder = new XmppConnectionBinder(); + private final List conversations = new CopyOnWriteArrayList<>(); + private final FileObserver fileObserver = new FileObserver( + FileBackend.getConversationsImageDirectory()) { + + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.DELETE) { + markFileDeleted(path.split("\\.")[0]); + } + } + }; + private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { + + @Override + public void onJinglePacketReceived(Account account, JinglePacket packet) { + mJingleConnectionManager.deliverPacket(account, packet); + } + }; + private final OnBindListener mOnBindListener = new OnBindListener() { + + @Override + public void onBind(final Account account) { + account.getRoster().clearPresences(); + account.pendingConferenceJoins.clear(); + account.pendingConferenceLeaves.clear(); + fetchRosterFromServer(account); + fetchBookmarks(account); + sendPresence(account); + connectMultiModeConversations(account); + updateConversationUi(); + } + }; + private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { + + @Override + public void onMessageAcknowledged(Account account, String uuid) { + for (final Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + Message message = conversation.findUnsentMessageWithUuid(uuid); + if (message != null) { + markMessage(message, Message.STATUS_SEND); + if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { + databaseBackend.updateConversation(conversation); + } + } + } + } + } + }; + private final IqGenerator mIqGenerator = new IqGenerator(this); + public DatabaseBackend databaseBackend; + public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { + + @Override + public void onContactStatusChanged(Contact contact, boolean online) { + Conversation conversation = find(getConversations(), contact); + if (conversation != null) { + if (online && contact.getPresences().size() > 1) { + conversation.endOtrIfNeeded(); + } else { + conversation.resetOtrSession(); + } + if (online && (contact.getPresences().size() == 1)) { + sendUnsentMessages(conversation); + } + } + } + }; + private FileBackend fileBackend = new FileBackend(this); + private MemorizingTrustManager mMemorizingTrustManager; + private NotificationService mNotificationService = new NotificationService( + this); + private OnMessagePacketReceived mMessageParser = new MessageParser(this); + private OnPresencePacketReceived 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 JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( + this); + private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( + this); + private AvatarService mAvatarService = new AvatarService(this); + private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); + private OnConversationUpdate mOnConversationUpdate = null; + private Integer convChangedListenerCount = 0; + private OnAccountUpdate mOnAccountUpdate = null; + private OnStatusChanged statusListener = new OnStatusChanged() { + + @Override + public void onStatusChanged(Account account) { + XmppConnection connection = account.getXmppConnection(); + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + if (account.getStatus() == Account.State.ONLINE) { + for (Conversation conversation : account.pendingConferenceLeaves) { + leaveMuc(conversation); + } + for (Conversation conversation : account.pendingConferenceJoins) { + joinMuc(conversation); + } + mMessageArchiveService.executePendingQueries(account); + mJingleConnectionManager.cancelInTransmission(); + List conversations = getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + conversation.startOtrIfNeeded(); + sendUnsentMessages(conversation); + } + } + if (connection != null && connection.getFeatures().csi()) { + if (checkListeners()) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + " sending csi//inactive"); + connection.sendInactive(); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + " sending csi//active"); + connection.sendActive(); + } + } + syncDirtyContacts(account); + scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode()); + } else if (account.getStatus() == Account.State.OFFLINE) { + resetSendingToWaiting(account); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + int timeToReconnect = mRandom.nextInt(50) + 10; + scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode()); + } + } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { + databaseBackend.updateAccount(account); + reconnectAccount(account, true); + } else if ((account.getStatus() != Account.State.CONNECTING) + && (account.getStatus() != Account.State.NO_INTERNET)) { + if (connection != null) { + int next = connection.getTimeToNextAttempt(); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": error connecting account. try again in " + + next + "s for the " + + (connection.getAttempt() + 1) + " time"); + scheduleWakeUpCall(next,account.getUuid().hashCode()); + } + } + getNotificationService().updateErrorNotification(); + } + }; + private int accountChangedListenerCount = 0; + private OnRosterUpdate mOnRosterUpdate = null; + private OnUpdateBlocklist mOnUpdateBlocklist = null; + private int updateBlocklistListenerCount = 0; + private int rosterChangedListenerCount = 0; + private OnMucRosterUpdate mOnMucRosterUpdate = null; + private int mucRosterChangedListenerCount = 0; + private SecureRandom mRandom; + private OpenPgpServiceConnection pgpServiceConnection; + private PgpEngine mPgpEngine = null; + private WakeLock wakeLock; + private PowerManager pm; + private LruCache mBitmapCache; + private Thread mPhoneContactMergerThread; + + private boolean mRestoredFromDatabase = false; + public boolean areMessagesInitialized() { + return this.mRestoredFromDatabase; + } + + 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 void attachLocationToConversation(final Conversation conversation, + final Uri uri, + final UiCallback callback) { + int encryption = conversation.getNextEncryption(forceEncryption()); + if (encryption == Message.ENCRYPTION_PGP) { + encryption = Message.ENCRYPTION_DECRYPTED; + } + Message message = new Message(conversation,uri.toString(),encryption); + if (conversation.getNextCounterpart() != null) { + message.setCounterpart(conversation.getNextCounterpart()); + } + if (encryption == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message,callback); + } else { + callback.success(message); + } + } + + public void attachFileToConversation(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.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_FILE); + message.setStatus(Message.STATUS_OFFERED); + String path = getFileBackend().getOriginalPath(uri); + if (path!=null) { + message.setRelativeFilePath(path); + getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } else { + new Thread(new Runnable() { + @Override + public void run() { + try { + getFileBackend().copyFileToPrivateStorage(message, uri); + getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileBackend.FileCopyException e) { + callback.error(e.getResId(),message); + } + } + }).start(); + + } + } + + public void 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.setCounterpart(conversation.getNextCounterpart()); + 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 (final FileBackend.FileCopyException e) { + callback.error(e.getResId(), message); + } + } + }).start(); + } + + public Conversation find(Bookmark bookmark) { + return find(bookmark.getAccount(), bookmark.getJid()); + } + + public Conversation find(final Account account, final Jid jid) { + return find(getConversations(), account, jid); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + final String action = intent == null ? null : intent.getAction(); + if (action != null) { + switch (action) { + case ACTION_MERGE_PHONE_CONTACTS: + if (mRestoredFromDatabase) { + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new CopyOnWriteArrayList(), + this); + } + return START_STICKY; + case Intent.ACTION_SHUTDOWN: + logoutAndSave(); + return START_NOT_STICKY; + case ACTION_CLEAR_NOTIFICATION: + mNotificationService.clear(); + break; + case ACTION_DISABLE_FOREGROUND: + getPreferences().edit().putBoolean("keep_foreground_service",false).commit(); + toggleForegroundService(); + break; + case ACTION_TRY_AGAIN: + for(Account account : accounts) { + if (account.hasErrorStatus()) { + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.resetAttemptCount(); + } + } + } + break; + case ACTION_DISABLE_ACCOUNT: + try { + String jid = intent.getStringExtra("account"); + Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid)); + if (account != null) { + account.setOption(Account.OPTION_DISABLED,true); + updateAccount(account); + } + } catch (final InvalidJidException ignored) { + break; + } + break; + } + } + this.wakeLock.acquire(); + + for (Account account : accounts) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (!hasInternetConnection()) { + account.setStatus(Account.State.NO_INTERNET); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } else { + if (account.getStatus() == Account.State.NO_INTERNET) { + account.setStatus(Account.State.OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + if (account.getStatus() == Account.State.ONLINE) { + long lastReceived = account.getXmppConnection().getLastPacketReceived(); + long lastSent = account.getXmppConnection().getLastPingSent(); + long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; + long msToNextPing = (Math.max(lastReceived,lastSent) + pingInterval) - SystemClock.elapsedRealtime(); + if (lastSent > lastReceived && (lastSent + Config.PING_TIMEOUT * 1000) < SystemClock.elapsedRealtime()) { + Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": ping timeout"); + this.reconnectAccount(account, true); + } else if (msToNextPing <= 0) { + account.getXmppConnection().sendPing(); + Log.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping"); + this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode()); + } else { + this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); + } + } else if (account.getStatus() == Account.State.OFFLINE) { + reconnectAccount(account,true); + } else if (account.getStatus() == Account.State.CONNECTING) { + long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000); + if (timeout < 0) { + Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting"); + reconnectAccount(account, true); + } else { + scheduleWakeUpCall((int) timeout,account.getUuid().hashCode()); + } + } else { + if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { + reconnectAccount(account, true); + } + } + + } + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + } + } + /*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE); + if (!pm.isScreenOn()) { + removeStaleListeners(); + }*/ + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (final RuntimeException ignored) { + } + } + return START_STICKY; + } + + public boolean hasInternetConnection() { + ConnectivityManager cm = (ConnectivityManager) getApplicationContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnected(); + } + + /** + * check whether we are allowed to download at the moment + */ + public boolean isDownloadAllowedInConnection() { + if (Settings.DOWNLOAD_ONLY_WLAN) { + return isWifiConnected(); + } + return true; + } + + /** + * check whether wifi is connected + */ + public boolean isWifiConnected() { + ConnectivityManager cm = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo niWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return niWifi.isConnected(); + } + + @SuppressLint("TrulyRandom") + @Override + public void onCreate() { + ExceptionHelper.init(getApplicationContext()); + PRNGFixes.apply(); + this.mRandom = new SecureRandom(); + this.mMemorizingTrustManager = new MemorizingTrustManager( + getApplicationContext()); + + final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + final int cacheSize = maxMemory / 8; + this.mBitmapCache = new LruCache(cacheSize) { + @Override + protected int sizeOf(final String key, final Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + + this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); + this.accounts = databaseBackend.getAccounts(); + + for (final Account account : this.accounts) { + account.initOtrEngine(this); + } + restoreFromDatabase(); + + 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"); + toggleForegroundService(); + } + + public void toggleForegroundService() { + if (getPreferences().getBoolean("keep_foreground_service",false)) { + startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification()); + } else { + stopForeground(true); + } + } + + @Override + public void onTaskRemoved(final Intent rootIntent) { + super.onTaskRemoved(rootIntent); + if (!getPreferences().getBoolean("keep_foreground_service",false)) { + this.logoutAndSave(); + } + } + + private void logoutAndSave() { + for (final 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, int requestCode) { + final long timeToWake = SystemClock.elapsedRealtime() + (seconds + 1) * 1000; + + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + Intent intent = new Intent(context, EventReceiver.class); + intent.setAction("ping"); + PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent); + } + + public XmppConnection createConnection(final Account account) { + final SharedPreferences sharedPref = getPreferences(); + account.setResource(sharedPref.getString("resource", "mobile") + .toLowerCase(Locale.getDefault())); + final 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); + connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); + return connection; + } + + public void sendChatState(Conversation conversation) { + if (sendChatStates()) { + MessagePacket packet = mMessageGenerator.generateChatState(conversation); + sendMessagePacket(conversation.getAccount(), packet); + } + } + + public void sendMessage(final Message message) { + final Account account = message.getConversation().getAccount(); + account.deactivateGracePeriod(); + final Conversation conv = message.getConversation(); + MessagePacket packet = null; + boolean saveInDb = true; + boolean send = false; + if (account.getStatus() == Account.State.ONLINE + && account.getXmppConnection() != null) { + if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + if (message.getCounterpart() != null) { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession()) { + conv.startOtrSession(message.getCounterpart().getResourcepart(),true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + mJingleConnectionManager + .createNewConnection(message); + } + } else { + mJingleConnectionManager.createNewConnection(message); + } + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + conv.startOtrIfNeeded(); + } + message.setStatus(Message.STATUS_WAITING); + } + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) { + conv.startOtrSession(message.getCounterpart().getResourcepart(), true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession()) { + if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + packet = mMessageGenerator.generateOtrChat(message); + send = true; + } else { + message.setStatus(Message.STATUS_WAITING); + conv.startOtrIfNeeded(); + } + } else { + message.setStatus(Message.STATUS_WAITING); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + message.getConversation().endOtrIfNeeded(); + message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + markMessage(message,Message.STATUS_SEND_FAILED); + } + }); + packet = mMessageGenerator.generatePgpChat(message); + send = true; + } else { + message.getConversation().endOtrIfNeeded(); + message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + markMessage(message,Message.STATUS_SEND_FAILED); + } + }); + 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.getCounterpart() != null) { + conv.startOtrSession(message.getCounterpart().getResourcepart(), false); + } + } + } + + } + conv.add(message); + if (saveInDb) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || saveEncryptedMessages()) { + databaseBackend.createMessage(message); + } + } + if ((send) && (packet != null)) { + if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (this.sendChatStates()) { + packet.addChild(ChatState.toElement(conv.getOutgoingChatState())); + } + } + sendMessagePacket(account, packet); + } + updateConversationUi(); + } + + private void sendUnsentMessages(final Conversation conversation) { + conversation.findWaitingMessages(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { + resendMessage(message); + } + }); + } + + private void resendMessage(final 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.getCounterpart() != null) + && (presences.has(message.getCounterpart().getResourcepart()))) { + message.getConversation().startOtrSession(message.getCounterpart().getResourcepart(), true); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.getConversation().startOtrSession(presence, true); + } + } + } else { + if (message.getConversation().getOtrSession() + .getSessionStatus() == SessionStatus.ENCRYPTED) { + try { + message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID())); + if (message.getType() == Message.TYPE_TEXT) { + packet = mMessageGenerator.generateOtrChat(message, + true); + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + mJingleConnectionManager.createNewConnection(message); + } + } catch (final InvalidJidException ignored) { + + } + } + } + } 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 || message.getType() == Message.TYPE_FILE) { + Contact contact = message.getConversation().getContact(); + Presences presences = contact.getPresences(); + if ((message.getCounterpart() != null) + && (presences.has(message.getCounterpart().getResourcepart()))) { + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + try { + message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence)); + } catch (InvalidJidException e) { + return; + } + 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); + } + if (message.getConversation().setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (this.sendChatStates()) { + packet.addChild(ChatState.toElement(message.getConversation().getOutgoingChatState())); + } + } + sendMessagePacket(account, packet); + } + } + + public void fetchRosterFromServer(final Account account) { + final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); + if (!"".equals(account.getRosterVersion())) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": fetching roster version " + account.getRosterVersion()); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); + } + iqPacket.query(Xmlns.ROSTER).setAttribute("ver", + account.getRosterVersion()); + account.getXmppConnection().sendIqPacket(iqPacket, mIqParser); + } + + public void fetchBookmarks(final Account account) { + final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); + final Element query = iqPacket.query("jabber:iq:private"); + query.addChild("storage", "storage:bookmarks"); + final OnIqPacketReceived callback = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final Element query = packet.query(); + final List bookmarks = new CopyOnWriteArrayList<>(); + final Element storage = query.findChild("storage", + "storage:bookmarks"); + if (storage != null) { + for (final Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + final Bookmark bookmark = Bookmark.parse(item, account); + bookmarks.add(bookmark); + Conversation conversation = find(bookmark); + if (conversation != null) { + conversation.setBookmark(bookmark); + } else if (bookmark.autojoin() && bookmark.getJid() != null) { + 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); + } + + public void onPhoneContactsLoaded(final List phoneContacts) { + if (mPhoneContactMergerThread != null) { + mPhoneContactMergerThread.interrupt(); + } + mPhoneContactMergerThread = new Thread(new Runnable() { + @Override + public void run() { + Log.d(Config.LOGTAG,"start merging phone contacts with roster"); + for (Account account : accounts) { + account.getRoster().clearSystemAccounts(); + for (Bundle phoneContact : phoneContacts) { + if (Thread.interrupted()) { + Log.d(Config.LOGTAG,"interrupted merging phone contacts"); + return; + } + Jid jid; + try { + jid = Jid.fromString(phoneContact.getString("jid")); + } catch (final InvalidJidException e) { + continue; + } + final Contact contact = account.getRoster().getContact(jid); + String systemAccount = phoneContact.getInt("phoneid") + + "#" + + phoneContact.getString("lookup"); + contact.setSystemAccount(systemAccount); + contact.setPhotoUri(phoneContact.getString("photouri")); + getAvatarService().clear(contact); + contact.setSystemName(phoneContact.getString("displayname")); + } + } + Log.d(Config.LOGTAG,"finished merging phone contacts"); + updateAccountUi(); + } + }); + mPhoneContactMergerThread.start(); + } + + private void restoreFromDatabase() { + synchronized (this.conversations) { + final Map accountLookupTable = new Hashtable<>(); + for (Account account : this.accounts) { + accountLookupTable.put(account.getUuid(), account); + } + this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); + for (Conversation conversation : this.conversations) { + Account account = accountLookupTable.get(conversation.getAccountUuid()); + conversation.setAccount(account); + } + new Thread(new Runnable() { + @Override + public void run() { + Log.d(Config.LOGTAG,"restoring roster"); + for(Account account : accounts) { + databaseBackend.readRoster(account.getRoster()); + } + getBitmapCache().evictAll(); + Looper.prepare(); + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new CopyOnWriteArrayList(), + XmppConnectionService.this); + Log.d(Config.LOGTAG,"restoring messages"); + for (Conversation conversation : conversations) { + conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); + checkDeletedFiles(conversation); + } + mRestoredFromDatabase = true; + Log.d(Config.LOGTAG,"restored all messages"); + updateConversationUi(); + } + }).start(); + } + } + + public List getConversations() { + return this.conversations; + } + + private void checkDeletedFiles(Conversation conversation) { + conversation.findMessagesWithFiles(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); + } + } + }); + } + + private void markFileDeleted(String uuid) { + for (Conversation conversation : getConversations()) { + Message message = conversation.findMessageWithFileAndUuid(uuid); + if (message != null) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); + updateConversationUi(); + } + return; + } + } + } + + public void populateWithOrderedConversations(final List list) { + populateWithOrderedConversations(list, true); + } + + public void populateWithOrderedConversations(final 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 void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { + Log.d(Config.LOGTAG,"load more messages for "+conversation.getName() + " prior to "+MessageGenerator.getTimestamp(timestamp)); + if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) { + return; + } + new Thread(new Runnable() { + @Override + public void run() { + final Account account = conversation.getAccount(); + List messages = databaseBackend.getMessages(conversation, 50,timestamp); + if (messages.size() > 0) { + conversation.addAll(0, messages); + checkDeletedFiles(conversation); + callback.onMoreMessagesLoaded(messages.size(), conversation); + } else if (conversation.hasMessagesLeftOnServer() + && account.isOnlineAndConnected() + && account.getXmppConnection().getFeatures().mam()) { + MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1); + if (query != null) { + query.setCallback(callback); + } + callback.informUser(R.string.fetching_history_from_server); + } + } + }).start(); + } + + public List getAccounts() { + return this.accounts; + } + + public Conversation find(final Iterable haystack, final Contact contact) { + for (final Conversation conversation : haystack) { + if (conversation.getContact() == contact) { + return conversation; + } + } + return null; + } + + public Conversation find(final Iterable haystack, final Account account, final Jid jid) { + if (jid == null) { + return null; + } + for (final Conversation conversation : haystack) { + if ((account == null || conversation.getAccount() == account) + && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) { + return conversation; + } + } + return null; + } + + public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) { + return this.findOrCreateConversation(account, jid, muc, null); + } + + public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) { + synchronized (this.conversations) { + 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); + conversation.setContactJid(jid); + } else { + conversation.setMode(Conversation.MODE_SINGLE); + conversation.setContactJid(jid.toBareJid()); + } + conversation.setNextEncryption(-1); + conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); + this.databaseBackend.updateConversation(conversation); + } else { + String conversationName; + Contact contact = account.getRoster().getContact(jid); + if (contact != null) { + conversationName = contact.getDisplayName(); + } else { + conversationName = jid.getLocalpart(); + } + if (muc) { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_MULTI); + } else { + conversation = new Conversation(conversationName, account, jid.toBareJid(), + Conversation.MODE_SINGLE); + } + this.databaseBackend.createConversation(conversation); + } + if (account.getXmppConnection() != null + && account.getXmppConnection().getFeatures().mam() + && !muc) { + if (query == null) { + this.mMessageArchiveService.query(conversation); + } else { + if (query.getConversation() == null) { + this.mMessageArchiveService.query(conversation, query.getStart()); + } + } + } + checkDeletedFiles(conversation); + this.conversations.add(conversation); + updateConversationUi(); + return conversation; + } + } + + public void archiveConversation(Conversation conversation) { + conversation.setStatus(Conversation.STATUS_ARCHIVED); + conversation.setNextEncryption(-1); + synchronized (this.conversations) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getAccount().getStatus() == Account.State.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 createAccount(final Account account) { + account.initOtrEngine(this); + databaseBackend.createAccount(account); + this.accounts.add(account); + this.reconnectAccountInBackground(account); + updateAccountUi(); + } + + public void updateAccount(final Account account) { + this.statusListener.onStatusChanged(account); + databaseBackend.updateAccount(account); + reconnectAccount(account, false); + updateAccountUi(); + getNotificationService().updateErrorNotification(); + } + + public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { + final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); + sendIqPacket(account, iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.setPassword(newPassword); + databaseBackend.updateAccount(account); + callback.onPasswordChangeSucceeded(); + } else { + callback.onPasswordChangeFailed(); + } + } + }); + } + + public void deleteAccount(final Account account) { + synchronized (this.conversations) { + for (final 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(); + getNotificationService().updateErrorNotification(); + } + } + + public void setOnConversationListChangedListener(OnConversationUpdate listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnConversationUpdate = listener; + this.mNotificationService.setIsInForeground(true); + if (this.convChangedListenerCount < 2) { + this.convChangedListenerCount++; + } + } + } + + public void removeOnConversationListChangedListener() { + synchronized (this) { + this.convChangedListenerCount--; + if (this.convChangedListenerCount <= 0) { + this.convChangedListenerCount = 0; + this.mOnConversationUpdate = null; + this.mNotificationService.setIsInForeground(false); + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnAccountListChangedListener(OnAccountUpdate listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnAccountUpdate = listener; + if (this.accountChangedListenerCount < 2) { + this.accountChangedListenerCount++; + } + } + } + + public void removeOnAccountListChangedListener() { + synchronized (this) { + this.accountChangedListenerCount--; + if (this.accountChangedListenerCount <= 0) { + this.mOnAccountUpdate = null; + this.accountChangedListenerCount = 0; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnRosterUpdateListener(final OnRosterUpdate listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnRosterUpdate = listener; + if (this.rosterChangedListenerCount < 2) { + this.rosterChangedListenerCount++; + } + } + } + + public void removeOnRosterUpdateListener() { + synchronized (this) { + this.rosterChangedListenerCount--; + if (this.rosterChangedListenerCount <= 0) { + this.rosterChangedListenerCount = 0; + this.mOnRosterUpdate = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnUpdateBlocklist = listener; + if (this.updateBlocklistListenerCount < 2) { + this.updateBlocklistListenerCount++; + } + } + } + + public void removeOnUpdateBlocklistListener() { + synchronized (this) { + this.updateBlocklistListenerCount--; + if (this.updateBlocklistListenerCount <= 0) { + this.updateBlocklistListenerCount = 0; + this.mOnUpdateBlocklist = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnMucRosterUpdate = listener; + if (this.mucRosterChangedListenerCount < 2) { + this.mucRosterChangedListenerCount++; + } + } + } + + public void removeOnMucRosterUpdateListener() { + synchronized (this) { + this.mucRosterChangedListenerCount--; + if (this.mucRosterChangedListenerCount <= 0) { + this.mucRosterChangedListenerCount = 0; + this.mOnMucRosterUpdate = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + private boolean checkListeners() { + return (this.mOnAccountUpdate == null + && this.mOnConversationUpdate == null + && this.mOnRosterUpdate == null + && this.mOnUpdateBlocklist == null); + } + + private void switchToForeground() { + for (Account account : getAccounts()) { + if (account.getStatus() == Account.State.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.State.ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().csi()) { + connection.sendInactive(); + } + } + } + for(Conversation conversation : getConversations()) { + conversation.setIncomingChatState(ChatState.ACTIVE); + } + this.mNotificationService.setIsInForeground(false); + Log.d(Config.LOGTAG, "app switched into background"); + } + + private void connectMultiModeConversations(Account account) { + List conversations = getConversations(); + for (Conversation conversation : conversations) { + if ((conversation.getMode() == Conversation.MODE_MULTI) + && (conversation.getAccount() == account)) { + conversation.resetMucOptions(); + joinMuc(conversation); + } + } + } + + public void joinMuc(Conversation conversation) { + Account account = conversation.getAccount(); + account.pendingConferenceJoins.remove(conversation); + account.pendingConferenceLeaves.remove(conversation); + if (account.getStatus() == Account.State.ONLINE) { + final String nick = conversation.getMucOptions().getProposedNick(); + final Jid joinJid = conversation.getMucOptions().createJoinJid(nick); + if (joinJid == null) { + return; //safety net + } + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString()); + PresencePacket packet = new PresencePacket(); + packet.setFrom(conversation.getAccount().getJid()); + packet.setTo(joinJid); + Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); + if (conversation.getMucOptions().getPassword() != null) { + x.addChild("password").setContent(conversation.getMucOptions().getPassword()); + } + x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted())); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + sendPresencePacket(account, packet); + fetchConferenceConfiguration(conversation); + if (!joinJid.equals(conversation.getJid())) { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + } + conversation.setHasMessagesLeftOnServer(false); + } else { + account.pendingConferenceJoins.add(conversation); + } + } + + 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 UiCallback callback) { + final MucOptions options = conversation.getMucOptions(); + final Jid joinJid = options.createJoinJid(nick); + if (options.online()) { + Account account = conversation.getAccount(); + options.setOnRenameListener(new OnRenameListener() { + + @Override + public void onSuccess() { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + pushBookmarks(bookmark.getAccount()); + } + callback.success(conversation); + } + + @Override + public void onFailure() { + callback.error(R.string.nick_in_use, conversation); + } + }); + + PresencePacket packet = new PresencePacket(); + packet.setTo(joinJid); + packet.setFrom(conversation.getAccount().getJid()); + + 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(joinJid); + databaseBackend.updateConversation(conversation); + if (conversation.getAccount().getStatus() == Account.State.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.State.ONLINE) { + PresencePacket packet = new PresencePacket(); + packet.setTo(conversation.getJid()); + packet.setFrom(conversation.getAccount().getJid()); + packet.setAttribute("type", "unavailable"); + sendPresencePacket(conversation.getAccount(), packet); + conversation.getMucOptions().setOffline(); + conversation.deregisterWithBookmark(); + Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + + ": leaving muc " + conversation.getJid()); + } else { + account.pendingConferenceLeaves.add(conversation); + } + } + + private String findConferenceServer(final Account account) { + String server; + if (account.getXmppConnection() != null) { + server = account.getXmppConnection().getMucServer(); + if (server != null) { + return server; + } + } + for (Account other : getAccounts()) { + if (other != account && other.getXmppConnection() != null) { + server = other.getXmppConnection().getMucServer(); + if (server != null) { + return server; + } + } + } + return null; + } + + public void createAdhocConference(final Account account, final Iterable jids, final UiCallback callback) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString()); + if (account.getStatus() == Account.State.ONLINE) { + try { + String server = findConferenceServer(account); + if (server == null) { + if (callback != null) { + callback.error(R.string.no_conference_server_found, null); + } + return; + } + String name = new BigInteger(75, getRNG()).toString(32); + Jid jid = Jid.fromParts(name, server, null); + final Conversation conversation = findOrCreateConversation(account, jid, true); + joinMuc(conversation); + Bundle options = new Bundle(); + options.putString("muc#roomconfig_persistentroom", "1"); + options.putString("muc#roomconfig_membersonly", "1"); + options.putString("muc#roomconfig_publicroom", "0"); + options.putString("muc#roomconfig_whois", "anyone"); + pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() { + @Override + public void onPushSucceeded() { + for (Jid invite : jids) { + invite(conversation, invite); + } + if (callback != null) { + callback.success(conversation); + } + } + + @Override + public void onPushFailed() { + if (callback != null) { + callback.error(R.string.conference_creation_failed, conversation); + } + } + }); + + } catch (InvalidJidException e) { + if (callback != null) { + callback.error(R.string.conference_creation_failed, null); + } + } + } else { + if (callback != null) { + callback.error(R.string.not_connected_try_again, null); + } + } + } + + public void fetchConferenceConfiguration(final Conversation conversation) { + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(conversation.getJid().toBareJid()); + request.query("http://jabber.org/protocol/disco#info"); + sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE.ERROR) { + ArrayList features = new ArrayList<>(); + for (Element child : packet.query().getChildren()) { + if (child != null && child.getName().equals("feature")) { + String var = child.getAttribute("var"); + if (var != null) { + features.add(var); + } + } + } + conversation.getMucOptions().updateFeatures(features); + updateConversationUi(); + } + } + }); + } + + public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) { + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(conversation.getJid().toBareJid()); + request.query("http://jabber.org/protocol/muc#owner"); + sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE.ERROR) { + Data data = Data.parse(packet.query().findChild("x", "jabber:x:data")); + for (Field field : data.getFields()) { + if (options.containsKey(field.getName())) { + field.setValue(options.getString(field.getName())); + } + } + data.submit(); + IqPacket set = new IqPacket(IqPacket.TYPE.SET); + set.setTo(conversation.getJid().toBareJid()); + set.query("http://jabber.org/protocol/muc#owner").addChild(data); + sendIqPacket(account, set, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + if (callback != null) { + callback.onPushSucceeded(); + } + } else { + if (callback != null) { + callback.onPushFailed(); + } + } + } + }); + } else { + if (callback != null) { + callback.onPushFailed(); + } + } + } + }); + } + + public void pushSubjectToConference(final Conversation conference, final String subject) { + MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject); + this.sendMessagePacket(conference.getAccount(), packet); + final MucOptions mucOptions = conference.getMucOptions(); + final MucOptions.User self = mucOptions.getSelf(); + if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { + Bundle options = new Bundle(); + options.putString("muc#roomconfig_persistentroom", "1"); + this.pushConferenceConfiguration(conference, options, null); + } + } + + public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { + final Jid jid = user.toBareJid(); + IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); + sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + callback.onAffiliationChangedSuccessful(jid); + } else { + callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); + } + } + }); + } + + public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) { + List jids = new ArrayList<>(); + for (MucOptions.User user : conference.getMucOptions().getUsers()) { + if (user.getAffiliation() == before) { + jids.add(user.getJid()); + } + } + IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString()); + sendIqPacket(conference.getAccount(), request, null); + } + + public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) { + IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); + Log.d(Config.LOGTAG, request.toString()); + sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, packet.toString()); + if (packet.getType() == IqPacket.TYPE.RESULT) { + callback.onRoleChangedSuccessful(nick); + } else { + callback.onRoleChangeFailed(nick, R.string.could_not_change_role); + } + } + }); + } + + public void disconnect(Account account, boolean force) { + if ((account.getStatus() == Account.State.ONLINE) + || (account.getStatus() == Account.State.DISABLED)) { + if (!force) { + List conversations = getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else { + if (conversation.endOtrIfNeeded()) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": ended otr session with " + + conversation.getJid()); + } + } + } + } + } + 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) { + final Account account = conversation.getAccount(); + final Session otrSession = conversation.getOtrSession(); + Log.d(Config.LOGTAG, + account.getJid().toBareJid() + " otr session established with " + + conversation.getJid() + "/" + + otrSession.getSessionID().getUserID()); + conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { + SessionID id = otrSession.getSessionID(); + try { + message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID())); + } catch (InvalidJidException e) { + return; + } + if (message.getType() == Message.TYPE_TEXT) { + MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true); + if (outPacket != null) { + message.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(message); + sendMessagePacket(account, outPacket); + } + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + mJingleConnectionManager.createNewConnection(message); + } + updateConversationUi(); + } + }); + } + + public boolean renewSymmetricKey(Conversation conversation) { + Account account = conversation.getAccount(); + byte[] symmetricKey = new byte[32]; + this.mRandom.nextBytes(symmetricKey); + Session otrSession = conversation.getOtrSession(); + if (otrSession != null) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(account.getJid()); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/" + + otrSession.getSessionID().getUserID()); + try { + packet.setBody(otrSession + .transformSending(CryptoHelper.FILETRANSFER + + CryptoHelper.bytesToHex(symmetricKey))[0]); + sendMessagePacket(account, packet); + conversation.setSymmetricKey(symmetricKey); + return true; + } catch (OtrException e) { + return false; + } + } + return false; + } + + public void pushContactToServer(final Contact contact) { + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.setOption(Contact.Options.DIRTY_PUSH); + final Account account = contact.getAccount(); + if (account.getStatus() == Account.State.ONLINE) { + final boolean ask = contact.getOption(Contact.Options.ASKING); + final boolean sendUpdates = contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) + && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.query(Xmlns.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(final Account account, + final 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; + } + final 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) { + final 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().toBareJid() + + ": 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().toBareJid().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().toBareJid() + + ": 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().toBareJid(); + 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.State.ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + Element item = iq.query(Xmlns.ROSTER).addChild("item"); + item.setAttribute("jid", contact.getJid().toString()); + 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) { + synchronized (account) { + 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(Config.CONNECT_TIMEOUT, account.getUuid().hashCode()); + } else { + account.getRoster().clearPresences(); + account.setXmppConnection(null); + } + } + } + + public void reconnectAccountInBackground(final Account account) { + new Thread(new Runnable() { + @Override + public void run() { + reconnectAccount(account,false); + } + }).start(); + } + + public void invite(Conversation conversation, Jid contact) { + MessagePacket packet = mMessageGenerator.invite(conversation, contact); + sendMessagePacket(conversation.getAccount(), packet); + } + + public void resetSendingToWaiting(Account account) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + conversation.findUnsentTextMessages(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { + markMessage(message, Message.STATUS_WAITING); + } + }); + } + } + } + + public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) { + if (uuid == null) { + return null; + } + for (Conversation conversation : getConversations()) { + if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) { + final Message message = conversation.findSentMessageWithUuid(uuid); + if (message != null) { + markMessage(message, status); + } + return message; + } + } + return null; + } + + public boolean markMessage(Conversation conversation, String uuid, + int status) { + if (uuid == null) { + return false; + } else { + Message message = conversation.findSentMessageWithUuid(uuid); + if (message != null) { + markMessage(message, status); + return true; + } else { + 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 sendChatStates() { + return getPreferences().getBoolean("chat_states", false); + } + + public boolean saveEncryptedMessages() { + return !getPreferences().getBoolean("dont_save_encrypted", false); + } + + public boolean indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + public int unreadCount() { + int count = 0; + for(Conversation conversation : getConversations()) { + count += conversation.unreadCount(); + } + return count; + } + + 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 void updateBlocklistUi(final OnUpdateBlocklist.Status status) { + if (mOnUpdateBlocklist != null) { + mOnUpdateBlocklist.OnUpdateBlocklist(status); + } + } + + public void updateMucRosterUi() { + if (mOnMucRosterUpdate != null) { + mOnMucRosterUpdate.onMucRosterUpdate(); + } + } + + public Account findAccountByJid(final Jid accountJid) { + for (Account account : this.accounts) { + if (account.getJid().toBareJid().equals(accountJid.toBareJid())) { + 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(final Conversation conversation) { + mNotificationService.clear(conversation); + conversation.markRead(); + } + + public void sendReadMarker(final Conversation conversation) { + final Message markable = conversation.getLatestMarkableMessage(); + this.markRead(conversation); + if (Settings.CONFIRM_MESSAGE_READ && markable != null && markable.getRemoteMsgId() != null) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); + Account account = conversation.getAccount(); + final Jid to = markable.getCounterpart(); + MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId()); + this.sendMessagePacket(conversation.getAccount(), packet); + } + updateConversationUi(); + } + + 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 syncRosterToDisk(final Account account) { + new Thread(new Runnable() { + + @Override + public void run() { + databaseBackend.writeRoster(account.getRoster()); + } + }).start(); + + } + + public List getKnownHosts() { + final List hosts = new ArrayList<>(); + for (final Account account : getAccounts()) { + if (!hosts.contains(account.getServer().toString())) { + hosts.add(account.getServer().toString()); + } + for (final Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster()) { + final String server = contact.getServer().toString(); + if (server != null && !hosts.contains(server)) { + hosts.add(server); + } + } + } + } + return hosts; + } + + public List getKnownConferenceHosts() { + final ArrayList mucServers = new ArrayList<>(); + for (final Account account : accounts) { + if (account.getXmppConnection() != null) { + final 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(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendIqPacket(packet, callback); + } + } + + public void sendPresence(final Account account) { + sendPresencePacket(account, mPresenceGenerator.sendPresence(account)); + } + + public MessageGenerator getMessageGenerator() { + return this.mMessageGenerator; + } + + public PresenceGenerator getPresenceGenerator() { + return this.mPresenceGenerator; + } + + public IqGenerator getIqGenerator() { + return this.mIqGenerator; + } + + public IqParser getIqParser() { + return this.mIqParser; + } + + public JingleConnectionManager getJingleConnectionManager() { + return this.mJingleConnectionManager; + } + + public MessageArchiveService getMessageArchiveService() { + return this.mMessageArchiveService; + } + + public List findContacts(Jid 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; + } + + public void resendFailedMessages(final Message message) { + final Collection messages = new ArrayList<>(); + Message current = message; + while (current.getStatus() == Message.STATUS_SEND_FAILED) { + messages.add(current); + if (current.mergeable(current.next())) { + current = current.next(); + } else { + break; + } + } + for (final Message msg : messages) { + markMessage(msg, Message.STATUS_WAITING); + this.resendMessage(msg); + } + } + + public void clearConversationHistory(final Conversation conversation) { + conversation.clearMessages(); + conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam + new Thread(new Runnable() { + @Override + public void run() { + databaseBackend.deleteMessagesInConversation(conversation); + } + }).start(); + } + + public void sendBlockRequest(final Blockable blockable) { + if (blockable != null && blockable.getBlockedJid() != null) { + final Jid jid = blockable.getBlockedJid(); + this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.getBlocklist().add(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + } + } + }); + } + } + + public void sendUnblockRequest(final Blockable blockable) { + if (blockable != null && blockable.getJid() != null) { + final Jid jid = blockable.getBlockedJid(); + this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.getBlocklist().remove(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + } + } + }); + } + } + + public interface OnMoreMessagesLoaded { + public void onMoreMessagesLoaded(int count, Conversation conversation); + + public void informUser(int r); + } + + public interface OnAccountPasswordChanged { + public void onPasswordChangeSucceeded(); + + public void onPasswordChangeFailed(); + } + + public interface OnAffiliationChanged { + public void onAffiliationChangedSuccessful(Jid jid); + + public void onAffiliationChangeFailed(Jid jid, int resId); + } + + public interface OnRoleChanged { + public void onRoleChangedSuccessful(String nick); + + public void onRoleChangeFailed(String nick, int resid); + } + + public interface OnConversationUpdate { + public void onConversationUpdate(); + } + + public interface OnAccountUpdate { + public void onAccountUpdate(); + } + + public interface OnRosterUpdate { + public void onRosterUpdate(); + } + + public interface OnMucRosterUpdate { + public void onMucRosterUpdate(); + } + + public interface OnConferenceOptionsPushed { + public void onPushSucceeded(); + + public void onPushFailed(); + } + + public class XmppConnectionBinder extends Binder { + public XmppConnectionService getService() { + return XmppConnectionService.this; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/AboutActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/AboutActivity.java new file mode 100644 index 00000000..fd8a187a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/AboutActivity.java @@ -0,0 +1,15 @@ +package de.thedevstack.conversationsplus.ui; + +import android.app.Activity; +import android.os.Bundle; + +import de.thedevstack.conversationsplus.R; + +public class AboutActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/AboutPreference.java b/src/main/java/de/thedevstack/conversationsplus/ui/AboutPreference.java new file mode 100644 index 00000000..c1fd4d67 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/AboutPreference.java @@ -0,0 +1,32 @@ +package de.thedevstack.conversationsplus.ui; + +import android.content.Context; +import android.content.Intent; +import android.preference.Preference; +import android.util.AttributeSet; + +import de.thedevstack.conversationsplus.utils.PhoneHelper; + +public class AboutPreference extends Preference { + public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + setSummary(); + } + + public AboutPreference(final Context context, final AttributeSet attrs) { + super(context, attrs); + setSummary(); + } + + @Override + protected void onClick() { + super.onClick(); + final Intent intent = new Intent(getContext(), AboutActivity.class); + getContext().startActivity(intent); + } + + private void setSummary() { + setSummary("Conversations+ " + PhoneHelper.getVersionName(getContext())); + } +} + diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/AbstractSearchableListItemActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/AbstractSearchableListItemActivity.java new file mode 100644 index 00000000..b8e57f47 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/AbstractSearchableListItemActivity.java @@ -0,0 +1,124 @@ +package de.thedevstack.conversationsplus.ui; + +import android.content.Context; +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.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.ListItem; +import de.thedevstack.conversationsplus.ui.adapter.ListItemAdapter; + +public abstract class AbstractSearchableListItemActivity extends XmppActivity { + private ListView mListView; + private final List listItems = new ArrayList<>(); + private ArrayAdapter mListItemsAdapter; + + private EditText mSearchEditText; + + private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(final MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(final MenuItem item) { + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filterContacts(); + return true; + } + }; + + private final TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(final Editable editable) { + filterContacts(editable.toString()); + } + + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, + final int after) { + } + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, + final int count) { + } + }; + + public ListView getListView() { + return mListView; + } + + public List getListItems() { + return listItems; + } + + public EditText getSearchEditText() { + return mSearchEditText; + } + + public ArrayAdapter getListItemAdapter() { + return mListItemsAdapter; + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_choose_contact); + mListView = (ListView) findViewById(R.id.choose_contact_list); + mListView.setFastScrollEnabled(true); + mListItemsAdapter = new ListItemAdapter(this, listItems); + mListView.setAdapter(mListItemsAdapter); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.choose_contact, menu); + final MenuItem menuSearchView = menu.findItem(R.id.action_search); + final View mSearchView = menuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + menuSearchView.setOnActionExpandListener(mOnActionExpandListener); + return true; + } + + protected void filterContacts() { + filterContacts(null); + } + + protected abstract void filterContacts(final String needle); + + @Override + void onBackendConnected() { + filterContacts(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/BlockContactDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/BlockContactDialog.java new file mode 100644 index 00000000..589f565c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/BlockContactDialog.java @@ -0,0 +1,41 @@ +package de.thedevstack.conversationsplus.ui; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Blockable; +import de.thedevstack.conversationsplus.services.XmppConnectionService; + +public final class BlockContactDialog { + public static void show(final Context context, + final XmppConnectionService xmppConnectionService, + final Blockable blockable) { + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final boolean isBlocked = blockable.isBlocked(); + builder.setNegativeButton(R.string.cancel, null); + + if (blockable.getJid().isDomainJid() || blockable.getAccount().isBlocked(blockable.getJid().toDomainJid())) { + builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain); + builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, + blockable.getJid().toDomainJid())); + } else { + builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact); + builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text, + blockable.getJid().toBareJid())); + } + builder.setPositiveButton(isBlocked ? R.string.unblock : R.string.block, new DialogInterface.OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (isBlocked) { + xmppConnectionService.sendUnblockRequest(blockable); + } else { + xmppConnectionService.sendBlockRequest(blockable); + } + } + }); + builder.create().show(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/BlocklistActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/BlocklistActivity.java new file mode 100644 index 00000000..4e6d7701 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/BlocklistActivity.java @@ -0,0 +1,75 @@ +package de.thedevstack.conversationsplus.ui; + +import android.os.Bundle; +import android.text.Editable; +import android.view.View; +import android.widget.AdapterView; + +import java.util.Collections; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class BlocklistActivity extends AbstractSearchableListItemActivity implements OnUpdateBlocklist { + + private Account account = null; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(final AdapterView parent, + final View view, + final int position, + final long id) { + BlockContactDialog.show(parent.getContext(), xmppConnectionService,(Contact) getListItems().get(position)); + return true; + } + }); + } + + @Override + public void onBackendConnected() { + for (final Account account : xmppConnectionService.getAccounts()) { + if (account.getJid().toString().equals(getIntent().getStringExtra("account"))) { + this.account = account; + break; + } + } + filterContacts(); + } + + @Override + protected void filterContacts(final String needle) { + getListItems().clear(); + if (account != null) { + for (final Jid jid : account.getBlocklist()) { + final Contact contact = account.getRoster().getContact(jid); + if (contact.match(needle) && contact.isBlocked()) { + getListItems().add(contact); + } + } + Collections.sort(getListItems()); + } + runOnUiThread(new Runnable() { + @Override + public void run() { + getListItemAdapter().notifyDataSetChanged(); + } + }); + } + + @Override + public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { + final Editable editable = getSearchEditText().getText(); + if (editable != null) { + filterContacts(editable.toString()); + } else { + filterContacts(); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ChangePasswordActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ChangePasswordActivity.java new file mode 100644 index 00000000..9d6b50fc --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ChangePasswordActivity.java @@ -0,0 +1,109 @@ +package de.thedevstack.conversationsplus.ui; + +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged { + + private Button mChangePasswordButton; + private View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mAccount != null) { + final String currentPassword = mCurrentPassword.getText().toString(); + final String newPassword = mNewPassword.getText().toString(); + final String newPasswordConfirm = mNewPasswordConfirm.getText().toString(); + if (!currentPassword.equals(mAccount.getPassword())) { + mCurrentPassword.requestFocus(); + mCurrentPassword.setError(getString(R.string.account_status_unauthorized)); + } else if (!newPassword.equals(newPasswordConfirm)) { + mNewPasswordConfirm.requestFocus(); + mNewPasswordConfirm.setError(getString(R.string.passwords_do_not_match)); + } else if (newPassword.isEmpty()) { + mNewPassword.requestFocus(); + mNewPassword.setError(getString(R.string.password_should_not_be_empty)); + } else if (newPassword.trim().isEmpty()) { + mNewPassword.requestFocus(); + mNewPassword.setError(getString(R.string.password_should_not_contain_only_spaces)); + } else { + mCurrentPassword.setError(null); + mNewPassword.setError(null); + mNewPasswordConfirm.setError(null); + xmppConnectionService.updateAccountPasswordOnServer(mAccount, newPassword, ChangePasswordActivity.this); + mChangePasswordButton.setEnabled(false); + mChangePasswordButton.setTextColor(getSecondaryTextColor()); + mChangePasswordButton.setText(R.string.updating); + } + } + } + }; + private EditText mCurrentPassword; + private EditText mNewPassword; + private EditText mNewPasswordConfirm; + private Account mAccount; + + @Override + void onBackendConnected() { + try { + final String jid = getIntent() == null ? null : getIntent().getStringExtra("account"); + if (jid != null) { + this.mAccount = xmppConnectionService.findAccountByJid(Jid.fromString(jid)); + } + } catch (final InvalidJidException ignored) { + + } + + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_change_password); + Button mCancelButton = (Button) findViewById(R.id.left_button); + mCancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + this.mChangePasswordButton = (Button) findViewById(R.id.right_button); + this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked); + this.mCurrentPassword = (EditText) findViewById(R.id.current_password); + this.mNewPassword = (EditText) findViewById(R.id.new_password); + this.mNewPasswordConfirm = (EditText) findViewById(R.id.new_password_confirm); + } + + @Override + public void onPasswordChangeSucceeded() { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(ChangePasswordActivity.this,R.string.password_changed,Toast.LENGTH_LONG).show(); + finish(); + } + }); + } + + @Override + public void onPasswordChangeFailed() { + runOnUiThread(new Runnable() { + @Override + public void run() { + mNewPassword.setError(getString(R.string.could_not_change_password)); + mChangePasswordButton.setEnabled(true); + mChangePasswordButton.setTextColor(getPrimaryTextColor()); + mChangePasswordButton.setText(R.string.change_password); + } + }); + + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ChooseContactActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ChooseContactActivity.java new file mode 100644 index 00000000..d51d23e1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ChooseContactActivity.java @@ -0,0 +1,152 @@ +package de.thedevstack.conversationsplus.ui; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView.MultiChoiceModeListener; +import android.widget.AdapterView; +import android.widget.ListView; + +import java.util.Set; +import java.util.HashSet; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.ListItem; + +public class ChooseContactActivity extends AbstractSearchableListItemActivity { + + private Set selected; + private Set filterContacts; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + filterContacts = new HashSet<>(); + String[] contacts = getIntent().getStringArrayExtra("filter_contacts"); + if (contacts != null) { + Collections.addAll(filterContacts, contacts); + } + + if (getIntent().getBooleanExtra("multiple", false)) { + getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() { + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.select_multiple, menu); + selected = new HashSet(); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch(item.getItemId()) { + case R.id.selection_submit: + final Intent request = getIntent(); + final Intent data = new Intent(); + data.putExtra("conversation", + request.getStringExtra("conversation")); + String[] selection = getSelectedContactJids(); + data.putExtra("contacts", selection); + data.putExtra("multiple", true); + setResult(RESULT_OK, data); + finish(); + return true; + } + return false; + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + Contact item = (Contact) getListItems().get(position); + if (checked) { + selected.add(item); + } else { + selected.remove(item); + } + int numSelected = selected.size(); + MenuItem selectButton = mode.getMenu().findItem(R.id.selection_submit); + String buttonText = getResources().getQuantityString(R.plurals.select_contact, + numSelected, numSelected); + selectButton.setTitle(buttonText); + } + }); + } + + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(final AdapterView parent, final View view, + final int position, final long id) { + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + final Intent request = getIntent(); + final Intent data = new Intent(); + final ListItem mListItem = getListItems().get(position); + data.putExtra("contact", mListItem.getJid().toString()); + String account = request.getStringExtra("account"); + if (account == null && mListItem instanceof Contact) { + account = ((Contact) mListItem).getAccount().getJid().toBareJid().toString(); + } + data.putExtra("account", account); + data.putExtra("conversation", + request.getStringExtra("conversation")); + data.putExtra("multiple", false); + setResult(RESULT_OK, data); + finish(); + } + }); + + } + + protected void filterContacts(final String needle) { + getListItems().clear(); + for (final Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.State.DISABLED) { + for (final Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && + !filterContacts.contains(contact.getJid().toBareJid().toString()) + && contact.match(needle)) { + getListItems().add(contact); + } + } + } + } + Collections.sort(getListItems()); + getListItemAdapter().notifyDataSetChanged(); + } + + private String[] getSelectedContactJids() { + List result = new ArrayList<>(); + for (Contact contact : selected) { + result.add(contact.getJid().toString()); + } + return result.toArray(new String[result.size()]); + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConferenceDetailsActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConferenceDetailsActivity.java new file mode 100644 index 00000000..553e5c89 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConferenceDetailsActivity.java @@ -0,0 +1,562 @@ +package de.thedevstack.conversationsplus.ui; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.IntentSender.SendIntentException; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.view.ContextMenu; +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; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.PgpEngine; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Bookmark; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.entities.MucOptions.User; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnMucRosterUpdate; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnConversationUpdate; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed { + public static final String ACTION_VIEW_MUC = "view_muc"; + private Conversation mConversation; + private OnClickListener inviteListener = new OnClickListener() { + + @Override + public void onClick(View v) { + inviteToConversation(mConversation); + } + }; + 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 TextView mConferenceType; + private ImageButton mChangeConferenceSettingsButton; + private Button mInviteButton; + private String uuid = null; + private User mSelectedUser = null; + + private boolean mAdvancedMode = false; + + private UiCallback renameCallback = new UiCallback() { + @Override + public void success(Conversation object) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(ConferenceDetailsActivity.this,getString(R.string.your_nick_has_been_changed),Toast.LENGTH_SHORT).show(); + updateView(); + } + }); + + } + + @Override + public void error(final int errorCode, Conversation object) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(ConferenceDetailsActivity.this,getString(errorCode),Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void userInputRequried(PendingIntent pi, Conversation object) { + + } + }; + private OnClickListener mChangeConferenceSettings = new OnClickListener() { + @Override + public void onClick(View v) { + final MucOptions mucOptions = mConversation.getMucOptions(); + AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this); + builder.setTitle(R.string.conference_options); + String[] options = {getString(R.string.members_only), + getString(R.string.non_anonymous)}; + final boolean[] values = new boolean[options.length]; + values[0] = mucOptions.membersOnly(); + values[1] = mucOptions.nonanonymous(); + builder.setMultiChoiceItems(options,values,new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + values[which] = isChecked; + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.confirm,new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (!mucOptions.membersOnly() && values[0]) { + xmppConnectionService.changeAffiliationsInConference(mConversation, + MucOptions.Affiliation.NONE, + MucOptions.Affiliation.MEMBER); + } + Bundle options = new Bundle(); + options.putString("muc#roomconfig_membersonly", values[0] ? "1" : "0"); + options.putString("muc#roomconfig_whois", values[1] ? "anyone" : "moderators"); + options.putString("muc#roomconfig_persistentroom", "1"); + xmppConnectionService.pushConferenceConfiguration(mConversation, + options, + ConferenceDetailsActivity.this); + } + }); + builder.create().show(); + } + }; + private OnValueEdited onSubjectEdited = new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + xmppConnectionService.pushSubjectToConference(mConversation,value); + } + }; + + @Override + public void onConversationUpdate() { + refreshUi(); + } + + @Override + public void onMucRosterUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + updateView(); + } + + @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); + mChangeConferenceSettingsButton = (ImageButton) findViewById(R.id.change_conference_button); + mChangeConferenceSettingsButton.setOnClickListener(this.mChangeConferenceSettings); + mConferenceType = (TextView) findViewById(R.id.muc_conference_type); + mInviteButton = (Button) findViewById(R.id.invite); + mInviteButton.setOnClickListener(inviteListener); + mConferenceType = (TextView) findViewById(R.id.muc_conference_type); + if (getActionBar() != null) { + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + } + mEditNickButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + quickEdit(mConversation.getMucOptions().getActualNick(), + new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + xmppConnectionService.renameInMuc(mConversation,value,renameCallback); + } + }); + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.action_edit_subject: + if (mConversation != null) { + quickEdit(mConversation.getName(),this.onSubjectEdited); + } + break; + case R.id.action_save_as_bookmark: + saveAsBookmark(); + break; + case R.id.action_delete_bookmark: + deleteBookmark(); + break; + case R.id.action_advanced_mode: + this.mAdvancedMode = !menuItem.isChecked(); + menuItem.setChecked(this.mAdvancedMode); + invalidateOptionsMenu(); + updateView(); + break; + } + return super.onOptionsItemSelected(menuItem); + } + + @Override + protected String getShareableUri() { + if (mConversation != null) { + return "xmpp:" + mConversation.getJid().toBareJid().toString() + "?join"; + } else { + return ""; + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark); + MenuItem menuItemDeleteBookmark = menu.findItem(R.id.action_delete_bookmark); + MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode); + menuItemAdvancedMode.setChecked(mAdvancedMode); + Account account = mConversation.getAccount(); + if (account.hasBookmarkFor(mConversation.getJid().toBareJid())) { + menuItemSaveBookmark.setVisible(false); + menuItemDeleteBookmark.setVisible(true); + } else { + menuItemDeleteBookmark.setVisible(false); + menuItemSaveBookmark.setVisible(true); + } + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.muc_details, menu); + return true; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + Object tag = v.getTag(); + if (tag instanceof User) { + getMenuInflater().inflate(R.menu.muc_details_context,menu); + final User user = (User) tag; + final User self = mConversation.getMucOptions().getSelf(); + this.mSelectedUser = user; + String name; + if (user.getJid() != null) { + final Contact contact = user.getContact(); + if (contact != null) { + name = contact.getDisplayName(); + } else { + name = user.getJid().toBareJid().toString(); + } + menu.setHeaderTitle(name); + MenuItem startConversation = menu.findItem(R.id.start_conversation); + MenuItem giveMembership = menu.findItem(R.id.give_membership); + MenuItem removeMembership = menu.findItem(R.id.remove_membership); + MenuItem giveAdminPrivileges = menu.findItem(R.id.give_admin_privileges); + MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges); + MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room); + MenuItem banFromConference = menu.findItem(R.id.ban_from_conference); + startConversation.setVisible(true); + if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && + self.getAffiliation().outranks(user.getAffiliation())) { + if (mAdvancedMode) { + if (user.getAffiliation() == MucOptions.Affiliation.NONE) { + giveMembership.setVisible(true); + } else { + removeMembership.setVisible(true); + } + banFromConference.setVisible(true); + } else { + removeFromRoom.setVisible(true); + } + if (user.getAffiliation() != MucOptions.Affiliation.ADMIN) { + giveAdminPrivileges.setVisible(true); + } else { + removeAdminPrivileges.setVisible(true); + } + } + } + + } + super.onCreateContextMenu(menu,v,menuInfo); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.start_conversation: + startConversation(mSelectedUser); + return true; + case R.id.give_admin_privileges: + xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.ADMIN,this); + return true; + case R.id.give_membership: + xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this); + return true; + case R.id.remove_membership: + xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.NONE,this); + return true; + case R.id.remove_admin_privileges: + xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this); + return true; + case R.id.remove_from_room: + removeFromRoom(mSelectedUser); + return true; + case R.id.ban_from_conference: + xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this); + xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this); + return true; + default: + return super.onContextItemSelected(item); + } + } + + private void removeFromRoom(final User user) { + if (mConversation.getMucOptions().membersOnly()) { + xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.NONE,this); + xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.ban_from_conference); + builder.setMessage(getString(R.string.removing_from_public_conference,user.getName())); + builder.setNegativeButton(R.string.cancel,null); + builder.setPositiveButton(R.string.ban_now,new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this); + xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this); + } + }); + builder.create().show(); + } + } + + protected void startConversation(User user) { + if (user.getJid() != null) { + Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getJid().toBareJid(),false); + switchToConversation(conversation); + } + } + + protected void saveAsBookmark() { + Account account = mConversation.getAccount(); + Bookmark bookmark = new Bookmark(account, mConversation.getJid().toBareJid()); + if (!mConversation.getJid().isBareJid()) { + bookmark.setNick(mConversation.getJid().getResourcepart()); + } + bookmark.setAutojoin(true); + account.getBookmarks().add(bookmark); + xmppConnectionService.pushBookmarks(account); + mConversation.setBookmark(bookmark); + } + + protected void deleteBookmark() { + Account account = mConversation.getAccount(); + Bookmark bookmark = mConversation.getBookmark(); + bookmark.unregisterConversation(); + account.getBookmarks().remove(bookmark); + xmppConnectionService.pushBookmarks(account); + } + + @Override + void onBackendConnected() { + if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { + this.uuid = getIntent().getExtras().getString("uuid"); + } + if (uuid != null) { + this.mConversation = xmppConnectionService + .findConversationByUuid(uuid); + if (this.mConversation != null) { + updateView(); + } + } + } + + private void updateView() { + final MucOptions mucOptions = mConversation.getMucOptions(); + final User self = mucOptions.getSelf(); + mAccountJid.setText(getString(R.string.using_account, mConversation + .getAccount().getJid().toBareJid())); + mYourPhoto.setImageBitmap(avatarService().get(mConversation.getAccount(), getPixel(48))); + setTitle(mConversation.getName()); + mFullJid.setText(mConversation.getJid().toBareJid().toString()); + mYourNick.setText(mucOptions.getActualNick()); + mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); + if (mucOptions.online()) { + mMoreDetails.setVisibility(View.VISIBLE); + final String status = getStatus(self); + if (status != null) { + mRoleAffiliaton.setVisibility(View.VISIBLE); + mRoleAffiliaton.setText(status); + } else { + mRoleAffiliaton.setVisibility(View.GONE); + } + if (mucOptions.membersOnly()) { + mConferenceType.setText(R.string.private_conference); + } else { + mConferenceType.setText(R.string.public_conference); + } + if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { + mChangeConferenceSettingsButton.setVisibility(View.VISIBLE); + } else { + mChangeConferenceSettingsButton.setVisibility(View.GONE); + } + } + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + membersView.removeAllViews(); + final ArrayList users = new ArrayList<>(); + users.addAll(mConversation.getMucOptions().getUsers()); + Collections.sort(users,new Comparator() { + @Override + public int compare(User lhs, User rhs) { + return lhs.getName().compareToIgnoreCase(rhs.getName()); + } + }); + for (final User user : users) { + View view = inflater.inflate(R.layout.contact, membersView,false); + this.setListItemBackgroundOnView(view); + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + highlightInMuc(mConversation, user.getName()); + } + }); + registerForContextMenu(view); + view.setTag(user); + TextView tvDisplayName = (TextView) view.findViewById(R.id.contact_display_name); + TextView tvKey = (TextView) view.findViewById(R.id.key); + TextView tvStatus = (TextView) view.findViewById(R.id.contact_jid); + if (mAdvancedMode && user.getPgpKeyId() != 0) { + tvKey.setVisibility(View.VISIBLE); + tvKey.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + viewPgpKey(user); + } + }); + tvKey.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); + } + Bitmap bm; + Contact contact = user.getContact(); + if (contact != null) { + bm = avatarService().get(contact, getPixel(48)); + tvDisplayName.setText(contact.getDisplayName()); + tvStatus.setText(user.getName() + " \u2022 " + getStatus(user)); + } else { + bm = avatarService().get(user.getName(), getPixel(48)); + tvDisplayName.setText(user.getName()); + tvStatus.setText(getStatus(user)); + + } + ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); + iv.setImageBitmap(bm); + membersView.addView(view); + if (mConversation.getMucOptions().canInvite()) { + mInviteButton.setVisibility(View.VISIBLE); + } else { + mInviteButton.setVisibility(View.GONE); + } + } + } + + private String getStatus(User user) { + if (mAdvancedMode) { + StringBuilder builder = new StringBuilder(); + builder.append(getString(user.getAffiliation().getResId())); + builder.append(" ("); + builder.append(getString(user.getRole().getResId())); + builder.append(')'); + return builder.toString(); + } else { + return getString(user.getAffiliation().getResId()); + } + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void setListItemBackgroundOnView(View view) { + int sdk = android.os.Build.VERSION.SDK_INT; + if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { + view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground)); + } else { + view.setBackground(getResources().getDrawable(R.drawable.greybackground)); + } + } + + private void viewPgpKey(User user) { + PgpEngine pgp = xmppConnectionService.getPgpEngine(); + if (pgp != null) { + PendingIntent intent = pgp.getIntentForKey( + mConversation.getAccount(), user.getPgpKeyId()); + if (intent != null) { + try { + startIntentSenderForResult(intent.getIntentSender(), 0, + null, 0, 0, 0); + } catch (SendIntentException ignored) { + + } + } + } + } + + @Override + public void onAffiliationChangedSuccessful(Jid jid) { + + } + + @Override + public void onAffiliationChangeFailed(Jid jid, int resId) { + displayToast(getString(resId,jid.toBareJid().toString())); + } + + @Override + public void onRoleChangedSuccessful(String nick) { + + } + + @Override + public void onRoleChangeFailed(String nick, int resId) { + displayToast(getString(resId,nick)); + } + + @Override + public void onPushSucceeded() { + displayToast(getString(R.string.modified_conference_options)); + } + + @Override + public void onPushFailed() { + displayToast(getString(R.string.could_not_modify_conference_options)); + } + + private void displayToast(final String msg) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(ConferenceDetailsActivity.this,msg,Toast.LENGTH_SHORT).show(); + } + }); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ContactDetailsActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ContactDetailsActivity.java new file mode 100644 index 00000000..2edad444 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ContactDetailsActivity.java @@ -0,0 +1,471 @@ +package de.thedevstack.conversationsplus.ui; + +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.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +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.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.QuickContactBadge; +import android.widget.TextView; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import java.util.List; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.PgpEngine; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.ListItem; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnAccountUpdate; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnRosterUpdate; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; +import de.thedevstack.conversationsplus.xmpp.XmppConnection; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist { + public static final String ACTION_VIEW_CONTACT = "view_contact"; + + private Contact contact; + private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteContactOnServer(contact); + } + }; + 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 Jid accountJid; + private Jid contactJid; + private TextView contactJidTv; + private TextView accountJidTv; + private TextView lastseen; + private CheckBox send; + private CheckBox receive; + private Button addContactButton; + private QuickContactBadge badge; + private LinearLayout keys; + private LinearLayout tags; + private boolean showDynamicTags; + + 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().toString()); + 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(); + } + }; + + @Override + public void onRosterUpdate() { + refreshUi(); + } + + @Override + public void onAccountUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + invalidateOptionsMenu(); + populateView(); + } + + @Override + protected String getShareableUri() { + if (contact != null) { + return contact.getShareableUri(); + } else { + return ""; + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { + try { + this.accountJid = Jid.fromString(getIntent().getExtras().getString("account")); + } catch (final InvalidJidException ignored) { + } + try { + this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact")); + } catch (final InvalidJidException ignored) { + } + } + setContentView(R.layout.activity_contact_details); + + contactJidTv = (TextView) findViewById(R.id.details_contactjid); + accountJidTv = (TextView) findViewById(R.id.details_account); + 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); + addContactButton = (Button) findViewById(R.id.add_contact_button); + addContactButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + showAddToRosterDialog(contact); + } + }); + keys = (LinearLayout) findViewById(R.id.details_contact_keys); + tags = (LinearLayout) findViewById(R.id.tags); + if (getActionBar() != null) { + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + } + + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem menuItem) { + final 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; + case R.id.action_block: + BlockContactDialog.show(this, xmppConnectionService, contact); + break; + case R.id.action_unblock: + BlockContactDialog.show(this, xmppConnectionService, contact); + break; + } + return super.onOptionsItemSelected(menuItem); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.contact_details, menu); + MenuItem block = menu.findItem(R.id.action_block); + MenuItem unblock = menu.findItem(R.id.action_unblock); + MenuItem edit = menu.findItem(R.id.action_edit_contact); + MenuItem delete = menu.findItem(R.id.action_delete_contact); + final XmppConnection connection = contact.getAccount().getXmppConnection(); + if (connection != null && connection.getFeatures().blocking()) { + if (this.contact.isBlocked()) { + menu.findItem(R.id.action_block).setVisible(false); + } else { + menu.findItem(R.id.action_unblock).setVisible(false); + } + } else { + menu.findItem(R.id.action_unblock).setVisible(false); + menu.findItem(R.id.action_block).setVisible(false); + } + if (!contact.showInRoster()) { + edit.setVisible(false); + delete.setVisible(false); + } + return true; + } + + private void populateView() { + setTitle(contact.getDisplayName()); + if (contact.showInRoster()) { + send.setVisibility(View.VISIBLE); + receive.setVisibility(View.VISIBLE); + addContactButton.setVisibility(View.GONE); + send.setOnCheckedChangeListener(null); + receive.setOnCheckedChangeListener(null); + + 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().isOnlineAndConnected()) { + receive.setEnabled(true); + send.setEnabled(true); + } else { + receive.setEnabled(false); + send.setEnabled(false); + } + + send.setOnCheckedChangeListener(this.mOnSendCheckedChange); + receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange); + } else { + addContactButton.setVisibility(View.VISIBLE); + send.setVisibility(View.GONE); + receive.setVisibility(View.GONE); + } + + if (contact.isBlocked() && !this.showDynamicTags) { + lastseen.setText(R.string.contact_blocked); + } else { + lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.lastseen.time)); + } + + if (contact.getPresences().size() > 1) { + contactJidTv.setText(contact.getJid() + " (" + + contact.getPresences().size() + ")"); + } else { + contactJidTv.setText(contact.getJid().toString()); + } + accountJidTv.setText(getString(R.string.using_account, contact + .getAccount().getJid().toBareJid())); + prepareContactBadge(badge, contact); + if (contact.getSystemAccount() == null) { + badge.setOnClickListener(onBadgeClick); + } + + keys.removeAllViews(); + boolean hasKeys = false; + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + for(final String otrFingerprint : contact.getOtrFingerprints()) { + hasKeys = true; + 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(CryptoHelper.prettifyFingerprint(otrFingerprint)); + keys.addView(view); + remove.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + confirmToDeleteFingerprint(otrFingerprint); + } + }); + } + if (contact.getPgpKeyId() != 0) { + hasKeys = true; + 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); + } + + List tagList = contact.getTags(); + if (tagList.size() == 0 || !this.showDynamicTags) { + tags.setVisibility(View.GONE); + } else { + tags.setVisibility(View.VISIBLE); + tags.removeAllViewsInLayout(); + for(final ListItem.Tag tag : tagList) { + final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tags,false); + tv.setText(tag.getName()); + tv.setBackgroundColor(tag.getColor()); + tags.addView(tv); + } + } + } + + 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() { + if ((accountJid != null) && (contactJid != null)) { + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + return; + } + this.contact = account.getRoster().getContact(contactJid); + populateView(); + } + } + + @Override + public void OnUpdateBlocklist(final Status status) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + invalidateOptionsMenu(); + populateView(); + } + }); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java new file mode 100644 index 00000000..123be06f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java @@ -0,0 +1,1115 @@ +package de.thedevstack.conversationsplus.ui; + +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.Intent; +import android.content.IntentSender.SendIntentException; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.support.v4.widget.SlidingPaneLayout; +import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +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; + +import net.java.otr4j.session.SessionStatus; + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Blockable; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnAccountUpdate; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnConversationUpdate; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnRosterUpdate; +import de.thedevstack.conversationsplus.ui.adapter.ConversationAdapter; +import de.thedevstack.conversationsplus.utils.ExceptionHelper; +import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; + +public class ConversationActivity extends XmppActivity + implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist { + + public static final String ACTION_DOWNLOAD = "eu.siacs.conversations.action.DOWNLOAD"; + + public static final String VIEW_CONVERSATION = "viewConversation"; + public static final String CONVERSATION = "conversationUuid"; + public static final String MESSAGE = "messageUuid"; + public static final String TEXT = "text"; + public static final String NICK = "nick"; + + public static final int REQUEST_SEND_MESSAGE = 0x0201; + public static final int REQUEST_DECRYPT_PGP = 0x0202; + 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_CHOOSE_FILE = 0x0303; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; + private static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; + private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; + private static final String STATE_PANEL_OPEN = "state_panel_open"; + private static final String STATE_PENDING_URI = "state_pending_uri"; + + private String mOpenConverstaion = null; + private boolean mPanelOpen = true; + private Uri mPendingImageUri = null; + private Uri mPendingFileUri = null; + private Uri mPendingGeoUri = null; + + private View mContentView; + + private List conversationList = new ArrayList<>(); + private Conversation mSelectedConversation = null; + private ListView listView; + private ConversationFragment mConversationFragment; + + private ArrayAdapter listAdapter; + + private Toast prepareFileToast; + + private boolean mActivityPaused = false; + private boolean mRedirected = true; + + public Conversation getSelectedConversation() { + return this.mSelectedConversation; + } + + public void setSelectedConversation(Conversation conversation) { + this.mSelectedConversation = conversation; + } + + public void showConversationsOverview() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.openPane(); + } + } + + @Override + protected String getShareableUri() { + Conversation conversation = getSelectedConversation(); + if (conversation != null) { + return conversation.getAccount().getShareableUri(); + } else { + return ""; + } + } + + 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(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + mOpenConverstaion = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null); + mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true); + String pending = savedInstanceState.getString(STATE_PENDING_URI, null); + if (pending != null) { + mPendingImageUri = Uri.parse(pending); + } + } + + setContentView(R.layout.fragment_conversations_overview); + + this.mConversationFragment = new ConversationFragment(); + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.selected_conversation, this.mConversationFragment, "conversation"); + transaction.commit(); + + listView = (ListView) findViewById(R.id.list); + this.listAdapter = new ConversationAdapter(this, conversationList); + listView.setAdapter(this.listAdapter); + + if (getActionBar() != null) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + } + + listView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View clickedView, + int position, long arg3) { + if (getSelectedConversation() != conversationList.get(position)) { + setSelectedConversation(conversationList.get(position)); + ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation()); + } + hideConversationsOverview(); + openConversation(); + } + }); + 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) { + updateActionBarTitle(); + invalidateOptionsMenu(); + hideKeyboard(); + if (xmppConnectionServiceBound) { + xmppConnectionService.getNotificationService() + .setOpenConversation(null); + } + closeContextMenu(); + } + + @Override + public void onPanelClosed(View arg0) { + openConversation(); + } + + @Override + public void onPanelSlide(View arg0, float arg1) { + // TODO Auto-generated method stub + + } + }); + } + } + + @Override + public void switchToConversation(Conversation conversation) { + setSelectedConversation(conversation); + runOnUiThread(new Runnable() { + @Override + public void run() { + ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation()); + openConversation(); + } + }); + } + + private void updateActionBarTitle() { + updateActionBarTitle(isConversationsOverviewHideable() && !isConversationsOverviewVisable()); + } + + private void updateActionBarTitle(boolean titleShouldBeName) { + final ActionBar ab = getActionBar(); + final Conversation conversation = getSelectedConversation(); + if (ab != null) { + if (titleShouldBeName && conversation != null) { + ab.setDisplayHomeAsUpEnabled(true); + ab.setHomeButtonEnabled(true); + if (conversation.getMode() == Conversation.MODE_SINGLE || useSubjectToIdentifyConference()) { + ab.setTitle(conversation.getName()); + } else { + ab.setTitle(conversation.getJid().toBareJid().toString()); + } + } else { + ab.setDisplayHomeAsUpEnabled(false); + ab.setHomeButtonEnabled(false); + ab.setTitle(R.string.app_name); + } + } + } + + private void openConversation() { + this.updateActionBarTitle(); + this.invalidateOptionsMenu(); + if (xmppConnectionServiceBound) { + final Conversation conversation = getSelectedConversation(); + xmppConnectionService.getNotificationService().setOpenConversation(conversation); + sendReadMarkerIfNecessary(conversation); + } + listAdapter.notifyDataSetChanged(); + } + + public void sendReadMarkerIfNecessary(final Conversation conversation) { + if (!mActivityPaused && conversation != null) { + if (!conversation.isRead()) { + xmppConnectionService.sendReadMarker(conversation); + } else { + xmppConnectionService.markRead(conversation); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.conversations, menu); + final MenuItem menuSecure = menu.findItem(R.id.action_security); + final MenuItem menuArchive = menu.findItem(R.id.action_archive); + final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details); + final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); + final MenuItem menuAttach = menu.findItem(R.id.action_attach_file); + final MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history); + final MenuItem menuAdd = menu.findItem(R.id.action_add); + final MenuItem menuInviteContact = menu.findItem(R.id.action_invite); + final MenuItem menuMute = menu.findItem(R.id.action_mute); + final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); + + 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); + menuUnmute.setVisible(false); + } else { + menuAdd.setVisible(!isConversationsOverviewHideable()); + if (this.getSelectedConversation() != null) { + if (this.getSelectedConversation().getLatestMessage() + .getEncryption() != Message.ENCRYPTION_NONE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + menuSecure.setIcon(R.drawable.ic_lock_outline_white_48dp); + } else { + menuSecure.setIcon(R.drawable.ic_action_secure); + } + } + if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { + menuContactDetails.setVisible(false); + menuAttach.setVisible(false); + menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite()); + } else { + menuMucDetails.setVisible(false); + } + if (this.getSelectedConversation().isMuted()) { + menuMute.setVisible(false); + } else { + menuUnmute.setVisible(false); + } + } + } + return true; + } + + private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { + if (attachmentChoice == ATTACHMENT_CHOICE_LOCATION && encryption != Message.ENCRYPTION_OTR) { + getSelectedConversation().setNextCounterpart(null); + Intent intent = new Intent("eu.siacs.conversations.location.request"); + startActivityForResult(intent,attachmentChoice); + } else { + selectPresence(getSelectedConversation(), new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + Intent intent = new Intent(); + boolean chooser = false; + switch (attachmentChoice) { + case ATTACHMENT_CHOICE_CHOOSE_IMAGE: + intent.setAction(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + chooser = true; + break; + case ATTACHMENT_CHOICE_TAKE_PHOTO: + mPendingImageUri = xmppConnectionService.getFileBackend().getTakePhotoUri(); + intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, mPendingImageUri); + break; + case ATTACHMENT_CHOICE_CHOOSE_FILE: + chooser = true; + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setAction(Intent.ACTION_GET_CONTENT); + break; + case ATTACHMENT_CHOICE_RECORD_VOICE: + intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + break; + case ATTACHMENT_CHOICE_LOCATION: + intent.setAction("eu.siacs.conversations.location.request"); + break; + } + if (intent.resolveActivity(getPackageManager()) != null) { + if (chooser) { + startActivityForResult( + Intent.createChooser(intent, getString(R.string.perform_action_with)), + attachmentChoice); + } else { + startActivityForResult(intent, attachmentChoice); + } + } + } + }); + } + } + + private void attachFile(final int attachmentChoice) { + final Conversation conversation = getSelectedConversation(); + final int encryption = conversation.getNextEncryption(forceEncryption()); + if (encryption == 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,encryption); + } + + @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,Message.ENCRYPTION_NONE); + } + }); + } + } + } else { + showInstallPgpDialog(); + } + } else { + selectPresenceToAttachFile(attachmentChoice,encryption); + } + } + + @Override + public boolean onOptionsItemSelected(final 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: + switchToContactDetails(getSelectedConversation().getContact()); + 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; + case R.id.action_unmute: + unmuteConversation(getSelectedConversation()); + break; + case R.id.action_block: + BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation()); + break; + case R.id.action_unblock: + BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation()); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } else { + return super.onOptionsItemSelected(item); + } + } + + public void endConversation(Conversation conversation) { + showConversationsOverview(); + xmppConnectionService.archiveConversation(conversation); + if (conversationList.size() > 0) { + setSelectedConversation(conversationList.get(0)); + this.mConversationFragment.reInit(getSelectedConversation()); + } 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); + } else { + updateConversationList(); + ConversationActivity.this.mConversationFragment.updateMessages(); + } + } + }); + 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); + if (new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null) { + attachFilePopup.getMenu().findItem(R.id.attach_record_voice).setVisible(false); + } + if (new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null) { + attachFilePopup.getMenu().findItem(R.id.attach_location).setVisible(false); + } + 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_choose_file: + attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE); + break; + case R.id.attach_record_voice: + attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + break; + case R.id.attach_location: + attachFile(ATTACHMENT_CHOICE_LOCATION); + break; + } + return false; + } + }); + attachFilePopup.show(); + } + + public void verifyOtrSessionDialog(final Conversation conversation, View view) { + if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { + Toast.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show(); + return; + } + if (view == null) { + return; + } + PopupMenu popup = new PopupMenu(this, view); + popup.inflate(R.menu.verification_choices); + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + Intent intent = new Intent(ConversationActivity.this, VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); + switch (menuItem.getItemId()) { + case R.id.scan_fingerprint: + intent.putExtra("mode",VerifyOTRActivity.MODE_SCAN_FINGERPRINT); + break; + case R.id.ask_question: + intent.putExtra("mode",VerifyOTRActivity.MODE_ASK_QUESTION); + break; + case R.id.manual_verification: + intent.putExtra("mode",VerifyOTRActivity.MODE_MANUAL_VERIFICATION); + break; + } + startActivity(intent); + return true; + } + }); + popup.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); + final int[] durations = getResources().getIntArray( + R.array.mute_options_durations); + builder.setItems(R.array.mute_options_descriptions, + new OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, final int which) { + final long till; + if (durations[which] == -1) { + till = Long.MAX_VALUE; + } else { + till = System.currentTimeMillis() + (durations[which] * 1000); + } + conversation.setMutedTill(till); + ConversationActivity.this.xmppConnectionService.databaseBackend + .updateConversation(conversation); + updateConversationList(); + ConversationActivity.this.mConversationFragment.updateMessages(); + invalidateOptionsMenu(); + } + }); + builder.create().show(); + } + + public void unmuteConversation(final Conversation conversation) { + conversation.setMutedTill(0); + this.xmppConnectionService.databaseBackend.updateConversation(conversation); + updateConversationList(); + ConversationActivity.this.mConversationFragment.updateMessages(); + invalidateOptionsMenu(); + } + + @Override + public void onBackPressed() { + if (!isConversationsOverviewVisable()) { + showConversationsOverview(); + } else { + moveTaskToBack(true); + } + } + + @Override + protected void onNewIntent(final Intent intent) { + if (xmppConnectionServiceBound) { + if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) { + handleViewConversationIntent(intent); + } + } else { + setIntent(intent); + } + } + + @Override + public void onStart() { + super.onStart(); + this.mRedirected = false; + if (this.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + if (conversationList.size() >= 1) { + this.onConversationUpdate(); + } + } + + @Override + public void onPause() { + super.onPause(); + this.mActivityPaused = true; + if (this.xmppConnectionServiceBound) { + this.xmppConnectionService.getNotificationService().setIsInForeground(false); + } + } + + @Override + public void onResume() { + super.onResume(); + final int theme = findTheme(); + final boolean usingEnterKey = usingEnterKey(); + if (this.mTheme != theme || usingEnterKey != mUsingEnterKey) { + recreate(); + } + this.mActivityPaused = false; + if (this.xmppConnectionServiceBound) { + this.xmppConnectionService.getNotificationService().setIsInForeground(true); + } + + if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) { + sendReadMarkerIfNecessary(getSelectedConversation()); + } + + } + + @Override + public void onSaveInstanceState(final Bundle savedInstanceState) { + Conversation conversation = getSelectedConversation(); + if (conversation != null) { + savedInstanceState.putString(STATE_OPEN_CONVERSATION, + conversation.getUuid()); + } + savedInstanceState.putBoolean(STATE_PANEL_OPEN, + isConversationsOverviewVisable()); + if (this.mPendingImageUri != null) { + savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUri.toString()); + } + super.onSaveInstanceState(savedInstanceState); + } + + @Override + void onBackendConnected() { + this.xmppConnectionService.getNotificationService().setIsInForeground(true); + updateConversationList(); + if (xmppConnectionService.getAccounts().size() == 0) { + if (!mRedirected) { + this.mRedirected = true; + startActivity(new Intent(this, EditAccountActivity.class)); + finish(); + } + } else if (conversationList.size() <= 0) { + if (!mRedirected) { + this.mRedirected = true; + Intent intent = new Intent(this, StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); + finish(); + } + } else if (getIntent() != null && VIEW_CONVERSATION.equals(getIntent().getType())) { + handleViewConversationIntent(getIntent()); + } else if (selectConversationByUuid(mOpenConverstaion)) { + if (mPanelOpen) { + showConversationsOverview(); + } else { + if (isConversationsOverviewHideable()) { + openConversation(); + } + } + this.mConversationFragment.reInit(getSelectedConversation()); + mOpenConverstaion = null; + } else if (getSelectedConversation() != null) { + this.mConversationFragment.reInit(getSelectedConversation()); + } else { + showConversationsOverview(); + mPendingImageUri = null; + mPendingFileUri = null; + mPendingGeoUri = null; + setSelectedConversation(conversationList.get(0)); + this.mConversationFragment.reInit(getSelectedConversation()); + } + + if (mPendingImageUri != null) { + attachImageToConversation(getSelectedConversation(),mPendingImageUri); + mPendingImageUri = null; + } else if (mPendingFileUri != null) { + attachFileToConversation(getSelectedConversation(),mPendingFileUri); + mPendingFileUri = null; + } else if (mPendingGeoUri != null) { + attachLocationToConversation(getSelectedConversation(),mPendingGeoUri); + mPendingGeoUri = null; + } + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); + setIntent(new Intent()); + } + + private void handleViewConversationIntent(final Intent intent) { + final String uuid = (String) intent.getExtras().get(CONVERSATION); + final String downloadUuid = (String) intent.getExtras().get(MESSAGE); + final String text = intent.getExtras().getString(TEXT, ""); + final String nick = intent.getExtras().getString(NICK, null); + if (selectConversationByUuid(uuid)) { + this.mConversationFragment.reInit(getSelectedConversation()); + if (nick != null) { + this.mConversationFragment.highlightInConference(nick); + } else { + this.mConversationFragment.appendText(text); + } + hideConversationsOverview(); + openConversation(); + if (mContentView instanceof SlidingPaneLayout) { + updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet + } + if (downloadUuid != null) { + final Message message = mSelectedConversation.findMessageWithFileAndUuid(downloadUuid); + if (message != null) { + mConversationFragment.messageListAdapter.startDownloadable(message); + } + } + } + } + + private boolean selectConversationByUuid(String uuid) { + if (uuid == null) { + return false; + } + for (Conversation aConversationList : conversationList) { + if (aConversationList.getUuid().equals(uuid)) { + setSelectedConversation(aConversationList); + return true; + } + } + return false; + } + + @Override + protected void unregisterListeners() { + super.unregisterListeners(); + xmppConnectionService.getNotificationService().setOpenConversation(null); + } + + @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) { + mConversationFragment.hideSnackbar(); + mConversationFragment.updateMessages(); + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + mPendingImageUri = data.getData(); + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(),mPendingImageUri); + mPendingImageUri = null; + } + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) { + mPendingFileUri = data.getData(); + if (xmppConnectionServiceBound) { + attachFileToConversation(getSelectedConversation(),mPendingFileUri); + mPendingFileUri = null; + } + } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO && mPendingImageUri != null) { + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(),mPendingImageUri); + mPendingImageUri = null; + } + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(mPendingImageUri); + sendBroadcast(intent); + } else if (requestCode == ATTACHMENT_CHOICE_LOCATION) { + double latitude = data.getDoubleExtra("latitude",0); + double longitude = data.getDoubleExtra("longitude",0); + this.mPendingGeoUri = Uri.parse("geo:"+String.valueOf(latitude)+","+String.valueOf(longitude)); + if (xmppConnectionServiceBound) { + attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); + this.mPendingGeoUri = null; + } + } + } else { + if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { + mPendingImageUri = null; + } + } + } + + private void attachLocationToConversation(Conversation conversation, Uri uri) { + xmppConnectionService.attachLocationToConversation(conversation,uri, new UiCallback() { + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message object) { + + } + + @Override + public void userInputRequried(PendingIntent pi, Message object) { + + } + }); + } + + private void attachFileToConversation(Conversation conversation, Uri uri) { + prepareFileToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_file), Toast.LENGTH_LONG); + prepareFileToast.show(); + xmppConnectionService.attachFileToConversation(conversation,uri, new UiCallback() { + @Override + public void success(Message message) { + hidePrepareFileToast(); + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message message) { + displayErrorDialog(errorCode); + } + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + + } + }); + } + + private void attachImageToConversation(Conversation conversation, Uri uri) { + prepareFileToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), Toast.LENGTH_LONG); + prepareFileToast.show(); + xmppConnectionService.attachImageToConversation(conversation, uri, + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Message object) { + hidePrepareFileToast(); + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + hidePrepareFileToast(); + displayErrorDialog(error); + } + }); + } + + private void hidePrepareFileToast() { + if (prepareFileToast != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + prepareFileToast.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 (final SendIntentException ignored) { + } + } + + 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 indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + @Override + protected void refreshUiReal() { + updateConversationList(); + if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 0) { + if (!mRedirected) { + this.mRedirected = true; + startActivity(new Intent(this, EditAccountActivity.class)); + finish(); + } + } else if (conversationList.size() == 0) { + if (!mRedirected) { + this.mRedirected = true; + Intent intent = new Intent(this, StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); + finish(); + } + } else { + ConversationActivity.this.mConversationFragment.updateMessages(); + updateActionBarTitle(); + } + } + + @Override + public void onAccountUpdate() { + this.refreshUi(); + } + + @Override + public void onConversationUpdate() { + this.refreshUi(); + } + + @Override + public void onRosterUpdate() { + this.refreshUi(); + } + + @Override + public void OnUpdateBlocklist(Status status) { + this.refreshUi(); + runOnUiThread(new Runnable() { + @Override + public void run() { + invalidateOptionsMenu(); + } + }); + } + + public void unblockConversation(final Blockable conversation) { + xmppConnectionService.sendUnblockRequest(conversation); + } + + public boolean enterIsSend() { + return getPreferences().getBoolean("enter_is_send",false); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java new file mode 100644 index 00000000..ea62a288 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -0,0 +1,1185 @@ +package de.thedevstack.conversationsplus.ui; + +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.InputType; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +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; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import android.widget.Toast; + +import net.java.otr4j.session.SessionStatus; + +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentLinkedQueue; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.PgpEngine; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.DownloadablePlaceholder; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.entities.Presences; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.ui.XmppActivity.OnPresenceSelected; +import de.thedevstack.conversationsplus.ui.XmppActivity.OnValueEdited; +import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter; +import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter.OnContactPictureClicked; +import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import de.thedevstack.conversationsplus.utils.GeoHelper; +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import github.ankushsachdeva.emojicon.EmojiconGridView; +import github.ankushsachdeva.emojicon.EmojiconsPopup; +import github.ankushsachdeva.emojicon.emoji.Emojicon; + +public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener { + + protected Conversation conversation; + 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); + } + }); + } + }; + protected ListView messagesView; + final protected List messageList = new ArrayList<>(); + protected MessageAdapter messageListAdapter; + private EditMessage mEditMessage; + private ImageButton mSendButton; + private ImageView mEmojButton; + private View mRootView; + private EmojiconsPopup mEmojPopup; + private RelativeLayout snackbar; + private TextView snackbarMessage; + private TextView snackbarAction; + private boolean messagesLoaded = true; + private Toast messageLoaderToast; + + 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) { + synchronized (ConversationFragment.this.messageList) { + if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { + long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent(); + messagesLoaded = false; + activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { + @Override + public void onMoreMessagesLoaded(final int count, Conversation conversation) { + if (ConversationFragment.this.conversation != conversation) { + return; + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + final int oldPosition = messagesView.getFirstVisiblePosition(); + View v = messagesView.getChildAt(0); + final int pxOffset = (v == null) ? 0 : v.getTop(); + ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); + updateStatusMessages(); + messageListAdapter.notifyDataSetChanged(); + if (count != 0) { + final int newPosition = oldPosition + count; + int offset = 0; + try { + Message tmpMessage = messageList.get(newPosition); + + while(tmpMessage.wasMergedIntoPrevious()) { + offset++; + tmpMessage = tmpMessage.prev(); + } + } catch (final IndexOutOfBoundsException ignored) { + + } + messagesView.setSelectionFromTop(newPosition - offset, pxOffset); + messagesLoaded = true; + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + } + } + }); + } + + @Override + public void informUser(final int resId) { + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + if (ConversationFragment.this.conversation != conversation) { + return; + } + messageLoaderToast = Toast.makeText(activity,resId,Toast.LENGTH_LONG); + messageLoaderToast.show(); + } + }); + + } + }); + + } + } + } + }; + private IntentSender askForPassphraseIntent = null; + 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); + askForPassphraseIntent = null; + } catch (SendIntentException e) { + // + } + } + } + }; + protected OnClickListener clickToVerify = new OnClickListener() { + + @Override + public void onClick(View v) { + activity.verifyOtrSessionDialog(conversation,v); + } + }; + 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(); + } + }; + 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 ConversationActivity activity; + private Message selectedMessage; + + private void sendMessage() { + if (this.conversation == null) { + return; + } + if (mEditMessage.getText().length() < 1) { + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextCounterpart(null); + updateChatMsgHint(); + } + return; + } + Message message = new Message(conversation, mEditMessage.getText() + .toString(), conversation.getNextEncryption(activity + .forceEncryption())); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getNextCounterpart() != null) { + message.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_PRIVATE); + conversation.setNextCounterpart(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.getNextCounterpart() != null) { + this.mEditMessage.setHint(getString( + R.string.send_private_message_to, + conversation.getNextCounterpart().getResourcepart())); + } 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; + } + getActivity().invalidateOptionsMenu(); + } + } + + private void setupIme() { + if (((ConversationActivity)getActivity()).usingEnterKey()) { + mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE)); + } else { + mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE); + } + } + + @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); + setupIme(); + mEditMessage.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (activity != null) { + activity.hideConversationsOverview(); + } + } + }); + mEditMessage.setOnEditorActionListener(mEditorActionListener); + + // Start of emojicon + mEmojButton = (ImageView) view.findViewById(R.id.emoji_btn); + mRootView = view.findViewById(R.id.textsend); + + // Give the topmost view of your activity layout hierarchy. This will be used to measure soft keyboard height + mEmojPopup = new EmojiconsPopup(mRootView, this.getActivity()); + + //Will automatically set size according to the soft keyboard size + mEmojPopup.setSizeForSoftKeyboard(); + + //Set on emojicon click listener + mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() { + + @Override + public void onEmojiconClicked(Emojicon emojicon) { + mEditMessage.append(emojicon.getEmoji()); + } + }); + + //Set on backspace click listener + mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() { + + @Override + public void onEmojiconBackspaceClicked(View v) { + KeyEvent event = new KeyEvent( + 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); + mEditMessage.dispatchKeyEvent(event); + } + }); + + //If the emoji popup is dismissed, change emojiButton to smiley icon + mEmojPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { + + @Override + public void onDismiss() { + changeEmojiKeyboardIcon(mEmojButton, R.drawable.smiley); + } + }); + + //If the text keyboard closes, also dismiss the emoji popup + mEmojPopup.setOnSoftKeyboardOpenCloseListener(new EmojiconsPopup.OnSoftKeyboardOpenCloseListener() { + + @Override + public void onKeyboardOpen(int keyBoardHeight) { + + } + + @Override + public void onKeyboardClose() { + if (mEmojPopup.isShowing()) + mEmojPopup.dismiss(); + } + }); + + //On emoji clicked, add it to edittext + mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() { + + @Override + public void onEmojiconClicked(Emojicon emojicon) { + mEditMessage.append(emojicon.getEmoji()); + } + }); + + //On backspace clicked, emulate the KEYCODE_DEL key event + mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() { + + @Override + public void onEmojiconBackspaceClicked(View v) { + KeyEvent event = new KeyEvent( + 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); + mEditMessage.dispatchKeyEvent(event); + } + }); + + // To toggle between text keyboard and emoji keyboard keyboard(Popup) + mEmojButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + + //If popup is not showing => emoji keyboard is not visible, we need to show it + if(!mEmojPopup.isShowing()){ + + //If keyboard is visible, simply show the emoji popup + if(mEmojPopup.isKeyBoardOpen()){ + mEmojPopup.showAtBottom(); + changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard); + } + + //else, open the text keyboard first and immediately after that show the emoji popup + else{ + mEditMessage.setFocusableInTouchMode(true); + mEditMessage.requestFocus(); + mEmojPopup.showAtBottomPending(); + final InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT); + changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard); + } + } + + //If popup is showing, simply dismiss it to show the undelying text keyboard + else{ + mEmojPopup.dismiss(); + } + } + }); + + // End of emojicon + + 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.getCounterpart() != null) { + if (!message.getCounterpart().isBareJid()) { + highlightInConference(message.getCounterpart().getResourcepart()); + } else { + highlightInConference(message.getCounterpart().toString()); + } + } + } else { + activity.switchToContactDetails(message.getContact()); + } + } else { + Account account = message.getConversation().getAccount(); + Intent intent = new Intent(activity, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().toBareJid().toString()); + startActivity(intent); + } + } + }); + 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.getCounterpart() != null) { + privateMessageWith(message.getCounterpart()); + } + } + } else { + activity.showQrCode(); + } + } + }); + messagesView.setAdapter(messageListAdapter); + + registerForContextMenu(messagesView); + + return view; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + synchronized (this.messageList) { + super.onCreateContextMenu(menu, v, menuInfo); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedMessage = this.messageList.get(acmi.position); + populateContextMenu(menu); + } + } + + private void populateContextMenu(ContextMenu menu) { + final Message m = this.selectedMessage; + if (m.getType() != Message.TYPE_STATUS) { + activity.getMenuInflater().inflate(R.menu.message_context, menu); + menu.setHeaderTitle(R.string.message_options); + MenuItem copyText = menu.findItem(R.id.copy_text); + MenuItem shareWith = menu.findItem(R.id.share_with); + MenuItem sendAgain = menu.findItem(R.id.send_again); + MenuItem copyUrl = menu.findItem(R.id.copy_url); + MenuItem downloadImage = menu.findItem(R.id.download_image); + MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); + if ((m.getType() != Message.TYPE_TEXT && m.getType() != Message.TYPE_PRIVATE) + || m.getDownloadable() != null || GeoHelper.isGeoUri(m.getBody())) { + copyText.setVisible(false); + } + if ((m.getType() == Message.TYPE_TEXT + || m.getType() == Message.TYPE_PRIVATE + || m.getDownloadable() != null) + && (!GeoHelper.isGeoUri(m.getBody()))) { + shareWith.setVisible(false); + } + if (m.getStatus() != Message.STATUS_SEND_FAILED) { + sendAgain.setVisible(false); + } + if (((m.getType() != Message.TYPE_IMAGE && m.getDownloadable() == null) + || m.getImageParams().url == null) && !GeoHelper.isGeoUri(m.getBody())) { + copyUrl.setVisible(false); + } + if (m.getType() != Message.TYPE_TEXT + || m.getDownloadable() != null + || !m.bodyContainsDownloadable()) { + downloadImage.setVisible(false); + } + if (!((m.getDownloadable() != null && !(m.getDownloadable() instanceof DownloadablePlaceholder)) + || (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING + || m.getStatus() == Message.STATUS_OFFERED)))) { + cancelTransmission.setVisible(false); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.share_with: + shareWith(selectedMessage); + return true; + case R.id.copy_text: + copyText(selectedMessage); + return true; + case R.id.send_again: + resendMessage(selectedMessage); + return true; + case R.id.copy_url: + copyUrl(selectedMessage); + return true; + case R.id.download_image: + downloadImage(selectedMessage); + return true; + case R.id.cancel_transmission: + cancelTransmission(selectedMessage); + return true; + default: + return super.onContextItemSelected(item); + } + } + + private void shareWith(Message message) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + if (GeoHelper.isGeoUri(message.getBody())) { + shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); + shareIntent.setType("text/plain"); + } else { + shareIntent.putExtra(Intent.EXTRA_STREAM, + activity.xmppConnectionService.getFileBackend() + .getJingleFileUri(message)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + String path = message.getRelativeFilePath(); + String mime = path == null ? null : URLConnection.guessContentTypeFromName(path); + if (mime == null) { + mime = "image/webp"; + } + shareIntent.setType(mime); + } + activity.startActivity(Intent.createChooser(shareIntent,getText(R.string.share_with))); + } + + private void copyText(Message message) { + if (activity.copyTextToClipboard(message.getMergedBody(), + R.string.message_text)) { + Toast.makeText(activity, R.string.message_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + } + + private void resendMessage(Message message) { + if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { + DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + if (!file.exists()) { + Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); + message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); + return; + } + } + activity.xmppConnectionService.resendFailedMessages(message); + } + + private void copyUrl(Message message) { + final String url; + final int resId; + if (GeoHelper.isGeoUri(message.getBody())) { + resId = R.string.location; + url = message.getBody(); + } else { + resId = R.string.image_url; + url = message.getImageParams().url.toString(); + } + if (activity.copyTextToClipboard(url, resId)) { + Toast.makeText(activity, R.string.url_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + } + + private void downloadImage(Message message) { + activity.xmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + } + + private void cancelTransmission(Message message) { + Downloadable downloadable = message.getDownloadable(); + if (downloadable!=null) { + downloadable.cancel(); + } else { + activity.xmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED); + } + } + + protected void privateMessageWith(final Jid counterpart) { + this.mEditMessage.setText(""); + this.conversation.setNextCounterpart(counterpart); + updateChatMsgHint(); + } + + protected void highlightInConference(String nick) { + String oldString = mEditMessage.getText().toString(); + 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 onStop() { + mDecryptJobRunning = false; + super.onStop(); + if (this.conversation != null) { + final String msg = mEditMessage.getText().toString(); + this.conversation.setNextMessage(msg); + updateChatState(this.conversation,msg); + } + } + + private void updateChatState(final Conversation conversation, final String msg) { + ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED; + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) { + activity.xmppConnectionService.sendChatState(conversation); + } + } + + public void reInit(Conversation conversation) { + if (conversation == null) { + return; + } + + this.activity = (ConversationActivity) getActivity(); + + if (this.conversation != null) { + final String msg = mEditMessage.getText().toString(); + this.conversation.setNextMessage(msg); + if (this.conversation != conversation) { + updateChatState(this.conversation,msg); + } + this.conversation.trim(); + } + + this.askForPassphraseIntent = null; + this.conversation = conversation; + this.mDecryptJobRunning = false; + this.mEncryptedMessages.clear(); + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + this.conversation.setNextCounterpart(null); + } + this.mEditMessage.setKeyboardListener(null); + this.mEditMessage.setText(""); + this.mEditMessage.append(this.conversation.getNextMessage()); + this.mEditMessage.setKeyboardListener(this); + this.messagesView.setAdapter(messageListAdapter); + updateMessages(); + this.messagesLoaded = true; + int size = this.messageList.size(); + if (size > 0) { + messagesView.setSelection(size - 1); + } + } + + private OnClickListener mUnblockClickListener = new OnClickListener() { + @Override + public void onClick(final View v) { + v.post(new Runnable() { + @Override + public void run() { + v.setVisibility(View.INVISIBLE); + } + }); + if (conversation.isDomainBlocked()) { + BlockContactDialog.show(activity, activity.xmppConnectionService, conversation); + } else { + activity.unblockConversation(conversation); + } + } + }; + + private OnClickListener mAddBackClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + final Contact contact = conversation == null ? null :conversation.getContact(); + if (contact != null) { + activity.xmppConnectionService.createContact(contact); + activity.switchToContactDetails(contact); + } + } + }; + + private OnClickListener mUnmuteClickListener = new OnClickListener() { + + @Override + public void onClick(final View v) { + activity.unmuteConversation(conversation); + } + }; + + private OnClickListener mAnswerSmpClickListener = new OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(activity, VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); + intent.putExtra("mode",VerifyOTRActivity.MODE_ANSWER_QUESTION); + startActivity(intent); + } + }; + + private void updateSnackBar(final Conversation conversation) { + final Account account = conversation.getAccount(); + final Contact contact = conversation.getContact(); + final int mode = conversation.getMode(); + if (conversation.isBlocked()) { + showSnackbar(R.string.contact_blocked, R.string.unblock,this.mUnblockClickListener); + } else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar(R.string.contact_added_you, R.string.add_back,this.mAddBackClickListener); + } else if (mode == Conversation.MODE_MULTI + &&!conversation.getMucOptions().online() + && account.getStatus() == Account.State.ONLINE) { + switch (conversation.getMucOptions().getError()) { + case MucOptions.ERROR_NICK_IN_USE: + showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc); + break; + case MucOptions.ERROR_UNKNOWN: + 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; + } + } else if (askForPassphraseIntent != null ) { + showSnackbar(R.string.openpgp_messages_found,R.string.decrypt, clickToDecryptListener); + } else if (mode == Conversation.MODE_SINGLE + && conversation.smpRequested()) { + showSnackbar(R.string.smp_requested, R.string.verify,this.mAnswerSmpClickListener); + } else if (mode == Conversation.MODE_SINGLE + &&conversation.hasValidOtrSession() + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) + && (!conversation.isOtrFingerprintVerified())) { + showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); + } else if (conversation.isMuted()) { + showSnackbar(R.string.notifications_disabled, R.string.enable,this.mUnmuteClickListener); + } else { + hideSnackbar(); + } + } + + public void updateMessages() { + synchronized (this.messageList) { + if (getView() == null) { + return; + } + final ConversationActivity activity = (ConversationActivity) getActivity(); + if (this.conversation != null) { + updateSnackBar(this.conversation); + final Contact contact = this.conversation.getContact(); + if (this.conversation.isBlocked()) { + + } else if (!contact.showInRoster() + && contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + + } else if (conversation.getMode() == Conversation.MODE_SINGLE) { + makeFingerprintWarning(); + } else if (!conversation.getMucOptions().online() + && conversation.getAccount().getStatus() == Account.State.ONLINE) { + + } else if (this.conversation.isMuted()) { + + } + conversation.populateWithMessages(ConversationFragment.this.messageList); + for (final Message message : this.messageList) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + && (message.getStatus() == Message.STATUS_RECEIVED || message + .getStatus() >= Message.STATUS_SEND) + && message.getDownloadable() == null) { + if (!mEncryptedMessages.contains(message)) { + mEncryptedMessages.add(message); + } + } + } + decryptNext(); + updateStatusMessages(); + this.messageListAdapter.notifyDataSetChanged(); + updateChatMsgHint(); + if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) { + activity.sendReadMarkerIfNecessary(conversation); + } + 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(); + updateSnackBar(conversation); + } + + @Override + public void success(Message message) { + mDecryptJobRunning = false; + try { + mEncryptedMessages.remove(); + } catch (final NoSuchElementException ignored) { + + } + activity.xmppConnectionService.updateMessage(message); + } + + @Override + public void error(int error, Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); + mDecryptJobRunning = false; + try { + mEncryptedMessages.remove(); + } catch (final NoSuchElementException ignored) { + + } + 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 (Settings.SHOW_ONLINE_STATUS && c != null + && c.getAccount().getStatus() == Account.State.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() { + synchronized (this.messageList) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + ChatState state = conversation.getIncomingChatState(); + if (state == ChatState.COMPOSING) { + this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName()))); + } else if (state == ChatState.PAUSED) { + this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName()))); + } else { + 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, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); + return; + } + } + } + } + } + } + } + + protected void makeFingerprintWarning() { + + } + + protected void showSnackbar(final int message, final int action, + final OnClickListener clickListener) { + snackbar.setVisibility(View.VISIBLE); + snackbar.setOnClickListener(null); + snackbarMessage.setText(message); + snackbarMessage.setOnClickListener(null); + snackbarAction.setVisibility(View.VISIBLE); + 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; + activity.selectPresence(message.getConversation(), + new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + message.setCounterpart(conversation.getNextCounterpart()); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + + public void appendText(String text) { + String previous = this.mEditMessage.getText().toString(); + if (previous.length() != 0 && !previous.endsWith(" ")) { + text = " " + text; + } + this.mEditMessage.append(text); + } + + @Override + public boolean onEnterPressed() { + if (activity.enterIsSend()) { + sendMessage(); + return true; + } else { + return false; + } + } + + @Override + public void onTypingStarted() { + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) { + activity.xmppConnectionService.sendChatState(conversation); + } + } + + @Override + public void onTypingStopped() { + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) { + activity.xmppConnectionService.sendChatState(conversation); + } + } + + @Override + public void onTextDeleted() { + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + activity.xmppConnectionService.sendChatState(conversation); + } + } + + private void changeEmojiKeyboardIcon(ImageView iconToBeChanged, int drawableResourceId){ + iconToBeChanged.setImageResource(drawableResourceId); + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/EditAccountActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/EditAccountActivity.java new file mode 100644 index 00000000..0023d6ed --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/EditAccountActivity.java @@ -0,0 +1,505 @@ +package de.thedevstack.conversationsplus.ui; + +import android.app.PendingIntent; +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.View.OnClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TableLayout; +import android.widget.TextView; +import android.widget.Toast; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnAccountUpdate; +import de.thedevstack.conversationsplus.ui.adapter.KnownHostsAdapter; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xmpp.XmppConnection.Features; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.pep.Avatar; + +public class EditAccountActivity extends XmppActivity implements OnAccountUpdate{ + + private AutoCompleteTextView mAccountJid; + private EditText mPassword; + private EditText mPasswordConfirm; + private CheckBox mRegisterNew; + private Button mCancelButton; + private Button mSaveButton; + private TableLayout mMoreTable; + + private LinearLayout mStats; + private TextView mServerInfoSm; + private TextView mServerInfoRosterVersion; + private TextView mServerInfoCarbons; + private TextView mServerInfoMam; + private TextView mServerInfoCSI; + private TextView mServerInfoBlocking; + private TextView mServerInfoPep; + private TextView mSessionEst; + private TextView mOtrFingerprint; + private ImageView mAvatar; + private RelativeLayout mOtrFingerprintBox; + private ImageButton mOtrFingerprintToClipboardButton; + + private Jid jidToEdit; + private Account mAccount; + + private boolean mFetchingAvatar = false; + + private final OnClickListener mSaveButtonClickListener = new OnClickListener() { + + @Override + public void onClick(final View v) { + if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED) { + mAccount.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(mAccount); + return; + } + final boolean registerNewAccount = mRegisterNew.isChecked(); + final Jid jid; + try { + jid = Jid.fromString(mAccountJid.getText().toString()); + } catch (final InvalidJidException e) { + mAccountJid.setError(getString(R.string.invalid_jid)); + mAccountJid.requestFocus(); + return; + } + if (jid.isDomainJid()) { + mAccountJid.setError(getString(R.string.invalid_jid)); + mAccountJid.requestFocus(); + return; + } + final String password = mPassword.getText().toString(); + final 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) { + try { + mAccount.setUsername(jid.hasLocalpart() ? jid.getLocalpart() : ""); + mAccount.setServer(jid.getDomainpart()); + } catch (final InvalidJidException ignored) { + return; + } + mAccountJid.setError(null); + mPasswordConfirm.setError(null); + mAccount.setPassword(password); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.updateAccount(mAccount); + } else { + try { + if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) { + mAccountJid.setError(getString(R.string.account_already_exists)); + mAccountJid.requestFocus(); + return; + } + } catch (final InvalidJidException e) { + return; + } + mAccount = new Account(jid.toBareJid(), 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 final OnClickListener mCancelButtonClickListener = new OnClickListener() { + + @Override + public void onClick(final View v) { + finish(); + } + }; + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + invalidateOptionsMenu(); + if (mAccount != null + && mAccount.getStatus() != Account.State.ONLINE + && mFetchingAvatar) { + startActivity(new Intent(getApplicationContext(), + ManageAccountActivity.class)); + finish(); + } else if (jidToEdit == null && mAccount != null + && mAccount.getStatus() == Account.State.ONLINE) { + if (!mFetchingAvatar) { + mFetchingAvatar = true; + xmppConnectionService.checkForAvatar(mAccount, + mAvatarFetchCallback); + } + } else { + updateSaveButton(); + } + if (mAccount != null) { + updateAccountInformation(); + } + } + }); + } + private final UiCallback mAvatarFetchCallback = new UiCallback() { + + @Override + public void userInputRequried(final PendingIntent pi, final Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void success(final Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void error(final int errorCode, final Avatar avatar) { + finishInitialSetup(avatar); + } + }; + private final TextWatcher mTextWatcher = new TextWatcher() { + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + updateSaveButton(); + } + + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + } + + @Override + public void afterTextChanged(final Editable s) { + + } + }; + + private final OnClickListener mAvatarClickListener = new OnClickListener() { + @Override + public void onClick(final View view) { + if (mAccount != null) { + final Intent intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", mAccount.getJid().toBareJid().toString()); + startActivity(intent); + } + } + }; + + protected void finishInitialSetup(final Avatar avatar) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + final Intent intent; + if (avatar != null) { + intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + intent.putExtra("init",true); + } else { + intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", mAccount.getJid().toBareJid().toString()); + intent.putExtra("setup", true); + } + startActivity(intent); + finish(); + } + }); + } + + protected void updateSaveButton() { + if (mAccount != null && (mAccount.getStatus() == Account.State.CONNECTING || mFetchingAvatar)) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + this.mSaveButton.setText(R.string.account_status_connecting); + } else if (mAccount != null && mAccount.getStatus() == Account.State.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.isOnlineAndConnected()) { + 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().toBareJid().toString().equals( + this.mAccountJid.getText().toString())) + || (!this.mAccount.getPassword().equals( + this.mPassword.getText().toString())); + } + + @Override + protected String getShareableUri() { + if (mAccount!=null) { + return mAccount.getShareableUri(); + } else { + return ""; + } + } + + @Override + protected void onCreate(final 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.mAvatar = (ImageView) findViewById(R.id.avater); + this.mAvatar.setOnClickListener(this.mAvatarClickListener); + 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.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version); + this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); + this.mServerInfoMam = (TextView) findViewById(R.id.server_info_mam); + this.mServerInfoCSI = (TextView) findViewById(R.id.server_info_csi); + this.mServerInfoBlocking = (TextView) findViewById(R.id.server_info_blocking); + 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.mMoreTable = (TableLayout) findViewById(R.id.server_info_more); + final OnCheckedChangeListener OnCheckedShowConfirmPassword = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(final CompoundButton buttonView, + final boolean isChecked) { + if (isChecked) { + mPasswordConfirm.setVisibility(View.VISIBLE); + } else { + mPasswordConfirm.setVisibility(View.GONE); + } + updateSaveButton(); + } + }; + this.mRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.editaccount, menu); + final MenuItem showQrCode = menu.findItem(R.id.action_show_qr_code); + final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list); + final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more); + final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server); + if (mAccount != null && mAccount.isOnlineAndConnected()) { + if (!mAccount.getXmppConnection().getFeatures().blocking()) { + showBlocklist.setVisible(false); + } + if (!mAccount.getXmppConnection().getFeatures().register()) { + changePassword.setVisible(false); + } + } else { + showQrCode.setVisible(false); + showBlocklist.setVisible(false); + showMoreInfo.setVisible(false); + changePassword.setVisible(false); + } + return true; + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + try { + this.jidToEdit = Jid.fromString(getIntent().getStringExtra("jid")); + } catch (final InvalidJidException | NullPointerException ignored) { + this.jidToEdit = null; + } + if (this.jidToEdit != null) { + this.mRegisterNew.setVisibility(View.GONE); + if (getActionBar() != null) { + getActionBar().setTitle(getString(R.string.account_details)); + } + } else { + this.mAvatar.setVisibility(View.GONE); + if (getActionBar() != null) { + getActionBar().setTitle(R.string.action_add_account); + } + } + } + } + + @Override + protected void onBackendConnected() { + final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, + xmppConnectionService.getKnownHosts()); + if (this.jidToEdit != null) { + this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); + updateAccountInformation(); + } else if (this.xmppConnectionService.getAccounts().size() == 0) { + if (getActionBar() != null) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setDisplayShowHomeEnabled(false); + getActionBar().setHomeButtonEnabled(false); + } + this.mCancelButton.setEnabled(false); + this.mCancelButton.setTextColor(getSecondaryTextColor()); + } + this.mAccountJid.setAdapter(mKnownHostsAdapter); + updateSaveButton(); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_show_block_list: + final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class); + showBlocklistIntent.putExtra("account", mAccount.getJid().toString()); + startActivity(showBlocklistIntent); + break; + case R.id.action_server_info_show_more: + mMoreTable.setVisibility(item.isChecked() ? View.GONE : View.VISIBLE); + item.setChecked(!item.isChecked()); + break; + case R.id.action_change_password_on_server: + final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class); + changePasswordIntent.putExtra("account", mAccount.getJid().toString()); + startActivity(changePasswordIntent); + break; + } + return super.onOptionsItemSelected(item); + } + + private void updateAccountInformation() { + this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString()); + this.mPassword.setText(this.mAccount.getPassword()); + if (this.jidToEdit != null) { + this.mAvatar.setVisibility(View.VISIBLE); + this.mAvatar.setImageBitmap(avatarService().get(this.mAccount, getPixel(72))); + } + 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.isOnlineAndConnected() && !this.mFetchingAvatar) { + this.mStats.setVisibility(View.VISIBLE); + this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() + .getLastSessionEstablished())); + Features features = this.mAccount.getXmppConnection().getFeatures(); + if (features.rosterVersioning()) { + this.mServerInfoRosterVersion.setText(R.string.server_info_available); + } else { + this.mServerInfoRosterVersion.setText(R.string.server_info_unavailable); + } + if (features.carbons()) { + this.mServerInfoCarbons.setText(R.string.server_info_available); + } else { + this.mServerInfoCarbons + .setText(R.string.server_info_unavailable); + } + if (features.mam()) { + this.mServerInfoMam.setText(R.string.server_info_available); + } else { + this.mServerInfoMam.setText(R.string.server_info_unavailable); + } + if (features.csi()) { + this.mServerInfoCSI.setText(R.string.server_info_available); + } else { + this.mServerInfoCSI.setText(R.string.server_info_unavailable); + } + if (features.blocking()) { + this.mServerInfoBlocking.setText(R.string.server_info_available); + } else { + this.mServerInfoBlocking.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(); + if (fingerprint != null) { + this.mOtrFingerprintBox.setVisibility(View.VISIBLE); + this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint)); + this.mOtrFingerprintToClipboardButton + .setVisibility(View.VISIBLE); + this.mOtrFingerprintToClipboardButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View v) { + + if (copyTextToClipboard(fingerprint, R.string.otr_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.getStatus().getReadableId())); + this.mAccountJid.requestFocus(); + } else { + this.mAccountJid.setError(null); + } + this.mStats.setVisibility(View.GONE); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/EditMessage.java b/src/main/java/de/thedevstack/conversationsplus/ui/EditMessage.java new file mode 100644 index 00000000..5c2e6164 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/EditMessage.java @@ -0,0 +1,78 @@ +package de.thedevstack.conversationsplus.ui; + +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.KeyEvent; + +import de.thedevstack.conversationsplus.Config; +import github.ankushsachdeva.emojicon.EmojiconEditText; + +public class EditMessage extends EmojiconEditText { + + public EditMessage(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditMessage(Context context) { + super(context); + } + + protected Handler mTypingHandler = new Handler(); + + protected Runnable mTypingTimeout = new Runnable() { + @Override + public void run() { + if (isUserTyping && keyboardListener != null) { + keyboardListener.onTypingStopped(); + isUserTyping = false; + } + } + }; + + private boolean isUserTyping = false; + + protected KeyboardListener keyboardListener; + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (keyboardListener != null && keyboardListener.onEnterPressed()) { + return true; + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + super.onTextChanged(text,start,lengthBefore,lengthAfter); + if (this.mTypingHandler != null && this.keyboardListener != null) { + this.mTypingHandler.removeCallbacks(mTypingTimeout); + this.mTypingHandler.postDelayed(mTypingTimeout, Config.TYPING_TIMEOUT * 1000); + final int length = text.length(); + if (!isUserTyping && length > 0) { + this.isUserTyping = true; + this.keyboardListener.onTypingStarted(); + } else if (length == 0) { + this.isUserTyping = false; + this.keyboardListener.onTextDeleted(); + } + } + } + + public void setKeyboardListener(KeyboardListener listener) { + this.keyboardListener = listener; + if (listener != null) { + this.isUserTyping = false; + } + } + + public interface KeyboardListener { + public boolean onEnterPressed(); + public void onTypingStarted(); + public void onTypingStopped(); + public void onTextDeleted(); + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ManageAccountActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ManageAccountActivity.java new file mode 100644 index 00000000..4f4282af --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ManageAccountActivity.java @@ -0,0 +1,273 @@ +package de.thedevstack.conversationsplus.ui; + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnAccountUpdate; +import de.thedevstack.conversationsplus.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 implements OnAccountUpdate { + + protected Account selectedAccount = null; + + protected final List accountList = new ArrayList<>(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + + @Override + public void onAccountUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + synchronized (this.accountList) { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + } + invalidateOptionsMenu(); + 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().toBareJid().toString()); + } + + @Override + void onBackendConnected() { + this.accountList.clear(); + this.accountList.addAll(xmppConnectionService.getAccounts()); + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manageaccounts, menu); + MenuItem enableAll = menu.findItem(R.id.action_enable_all); + if (!accountsLeftToEnable()) { + enableAll.setVisible(false); + } + MenuItem disableAll = menu.findItem(R.id.action_disable_all); + if (!accountsLeftToDisable()) { + disableAll.setVisible(false); + } + 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); + return true; + 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; + case R.id.action_disable_all: + disableAllAccounts(); + break; + case R.id.action_enable_all: + enableAllAccounts(); + 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().toString()); + startActivity(intent); + } + + private void disableAllAccounts() { + List list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + list.add(account); + } + } + } + for(Account account : list) { + disableAccount(account); + } + } + + private boolean accountsLeftToDisable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + return true; + } + } + return false; + } + } + + private boolean accountsLeftToEnable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isOptionSet(Account.OPTION_DISABLED)) { + return true; + } + } + return false; + } + } + + private void enableAllAccounts() { + List list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isOptionSet(Account.OPTION_DISABLED)) { + list.add(account); + } + } + } + for(Account account : list) { + enableAccount(account); + } + } + + 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/src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java new file mode 100644 index 00000000..852b5175 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java @@ -0,0 +1,253 @@ +package de.thedevstack.conversationsplus.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 android.widget.Toast; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.utils.PhoneHelper; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.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 OnLongClickListener backToDefaultListener = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + avatarUri = defaultUri; + loadImageIntoPreview(defaultUri); + return true; + } + }; + 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) { + Intent intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); + } + Toast.makeText(PublishProfilePictureActivity.this, + R.string.avatar_has_been_published, + Toast.LENGTH_SHORT).show(); + 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) { + } + }; + + @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) { + Intent intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); + } + 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) { + Jid jid; + try { + jid = Jid.fromString(getIntent().getStringExtra("account")); + } catch (InvalidJidException e) { + jid = null; + } + 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().toBareJid().toString()); + } + } + + } + + @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/src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java new file mode 100644 index 00000000..27e086ce --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java @@ -0,0 +1,92 @@ +package de.thedevstack.conversationsplus.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.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) { + // need to synchronize the settings class first + Settings.synchronizeSettingsClassWithPreferences(getPreferences(), name); + switch (name) { + case "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.reconnectAccountInBackground(account); + } + } + } + break; + case "keep_foreground_service": + xmppConnectionService.toggleForegroundService(); + break; + case "confirm_messages_list": + if (xmppConnectionServiceBound) { + for (Account account : xmppConnectionService.getAccounts()) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + xmppConnectionService.sendPresence(account); + } + } + } + break; + } + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/SettingsFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/SettingsFragment.java new file mode 100644 index 00000000..a549ff94 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/SettingsFragment.java @@ -0,0 +1,65 @@ +package de.thedevstack.conversationsplus.ui; + +import android.app.Dialog; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import de.thedevstack.conversationsplus.R; + +public class SettingsFragment extends PreferenceFragment { + + //http://stackoverflow.com/questions/16374820/action-bar-home-button-not-functional-with-nested-preferencescreen/16800527#16800527 + private void initializeActionBar(PreferenceScreen preferenceScreen) { + final Dialog dialog = preferenceScreen.getDialog(); + + if (dialog != null) { + View homeBtn = dialog.findViewById(android.R.id.home); + + if (homeBtn != null) { + View.OnClickListener dismissDialogClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }; + + ViewParent homeBtnContainer = homeBtn.getParent(); + + if (homeBtnContainer instanceof FrameLayout) { + ViewGroup containerParent = (ViewGroup) homeBtnContainer.getParent(); + if (containerParent instanceof LinearLayout) { + ((LinearLayout) containerParent).setOnClickListener(dismissDialogClickListener); + } else { + ((FrameLayout) homeBtnContainer).setOnClickListener(dismissDialogClickListener); + } + } else { + homeBtn.setOnClickListener(dismissDialogClickListener); + } + } + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + super.onPreferenceTreeClick(preferenceScreen, preference); + if (preference instanceof PreferenceScreen) { + initializeActionBar((PreferenceScreen) preference); + } + return false; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java new file mode 100644 index 00000000..f6a7b4c5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java @@ -0,0 +1,219 @@ +package de.thedevstack.conversationsplus.ui; + +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; + +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.ui.adapter.ConversationAdapter; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class ShareWithActivity extends XmppActivity { + + private class Share { + public Uri uri; + public boolean image; + 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 attachFileCallback = 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); + + if (getActionBar() != null) { + 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(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add: + final Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onStart() { + final String type = getIntent().getType(); + final Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + if (type != null && uri != null && !type.equalsIgnoreCase("text/plain")) { + this.share.uri = uri; + this.share.image = type.startsWith("image/") || isImage(uri); + } else { + this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT); + } + if (xmppConnectionServiceBound) { + xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uri == null); + } + super.onStart(); + } + + protected boolean isImage(Uri uri) { + try { + String guess = URLConnection.guessContentTypeFromName(uri.toString()); + return (guess != null && guess.startsWith("image/")); + } catch (final StringIndexOutOfBoundsException ignored) { + return false; + } + } + + @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; + try { + account = xmppConnectionService.findAccountByJid(Jid.fromString(share.account)); + } catch (final InvalidJidException e) { + account = null; + } + if (account == null) { + return; + } + final Conversation conversation; + try { + conversation = xmppConnectionService + .findOrCreateConversation(account, Jid.fromString(share.contact), false); + } catch (final InvalidJidException e) { + return; + } + share(conversation); + } + + private void share(final Conversation conversation) { + if (share.uri != null) { + selectPresence(conversation, new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + if (share.image) { + Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), + Toast.LENGTH_LONG).show(); + ShareWithActivity.this.xmppConnectionService + .attachImageToConversation(conversation, share.uri, + attachFileCallback); + } else { + Toast.makeText(getApplicationContext(), + getText(R.string.preparing_file), + Toast.LENGTH_LONG).show(); + ShareWithActivity.this.xmppConnectionService + .attachFileToConversation(conversation, share.uri, + attachFileCallback); + } + switchToConversation(conversation, null, true); + finish(); + } + }); + + } else { + switchToConversation(conversation, this.share.text, true); + finish(); + } + + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/StartConversationActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/StartConversationActivity.java new file mode 100644 index 00000000..c0500375 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/StartConversationActivity.java @@ -0,0 +1,819 @@ +package de.thedevstack.conversationsplus.ui; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +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.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +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.Checkable; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Spinner; + +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Blockable; +import de.thedevstack.conversationsplus.entities.Bookmark; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.ListItem; +import de.thedevstack.conversationsplus.entities.Presences; +import de.thedevstack.conversationsplus.services.XmppConnectionService.OnRosterUpdate; +import de.thedevstack.conversationsplus.ui.adapter.KnownHostsAdapter; +import de.thedevstack.conversationsplus.ui.adapter.ListItemAdapter; +import de.thedevstack.conversationsplus.utils.XmppUri; +import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class StartConversationActivity extends XmppActivity implements OnRosterUpdate, OnUpdateBlocklist { + + public int conference_context_id; + public int contact_context_id; + 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 Invite mPendingInvite = null; + private Menu mOptionsMenu; + private EditText mSearchEditText; + 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 boolean mHideOfflineContacts = false; + 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) { + if (getActionBar() != null) { + getActionBar().setSelectedNavigationItem(position); + } + onTabChanged(); + } + }; + 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 MenuItem mMenuSearchView; + private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { + @Override + public void onTagClicked(String tag) { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(""); + mSearchEditText.append(tag); + filter(tag); + } + } + }; + private String mInitialJid; + + @Override + public void onRosterUpdate() { + this.refreshUi(); + } + + @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); + ((ListItemAdapter) mContactsAdapter).setOnTagClickedListener(this.mOnTagClickedListener); + 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); + } + }); + + this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false); + + } + + 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 toggleContactBlock() { + final int position = contact_context_id; + BlockContactDialog.show(this, xmppConnectionService, (Contact)contacts.get(position)); + } + + protected void deleteContact() { + final int position = contact_context_id; + final Contact contact = (Contact) contacts.get(position); + final 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(final String prefilledJid, final String fingerprint) { + 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); + if (fingerprint!=null) { + jid.setFocusable(false); + jid.setFocusableInTouchMode(false); + jid.setClickable(false); + jid.setCursorVisible(false); + } + } + 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(final View v) { + if (!xmppConnectionServiceBound) { + return; + } + final Jid accountJid; + try { + accountJid = Jid.fromString((String) spinner.getSelectedItem()); + } catch (final InvalidJidException e) { + return; + } + final Jid contactJid; + try { + contactJid = Jid.fromString(jid.getText().toString()); + } catch (final InvalidJidException e) { + jid.setError(getString(R.string.invalid_jid)); + return; + } + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + final Contact contact = account.getRoster().getContact(contactJid); + if (contact.showInRoster()) { + jid.setError(getString(R.string.contact_already_exists)); + } else { + contact.addOtrFingerprint(fingerprint); + xmppConnectionService.createContact(contact); + dialog.dismiss(); + switchToConversation(contact); + } + } + }); + + } + + @SuppressLint("InflateParams") + protected void showJoinConferenceDialog(final String prefilledJid) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.join_conference); + final 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)); + if (prefilledJid != null) { + jid.append(prefilledJid); + } + populateAccountSpinner(spinner); + final Checkable 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(final View v) { + if (!xmppConnectionServiceBound) { + return; + } + final Jid accountJid; + try { + accountJid = Jid.fromString((String) spinner.getSelectedItem()); + } catch (final InvalidJidException e) { + return; + } + final Jid conferenceJid; + try { + conferenceJid = Jid.fromString(jid.getText().toString()); + } catch (final InvalidJidException e) { + jid.setError(getString(R.string.invalid_jid)); + return; + } + final 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 { + final Bookmark bookmark = new Bookmark(account,conferenceJid.toBareJid()); + bookmark.setAutojoin(true); + account.getBookmarks().add(bookmark); + xmppConnectionService + .pushBookmarks(account); + final Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + conversation.setBookmark(bookmark); + if (!conversation.getMucOptions().online()) { + xmppConnectionService + .joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } else { + final Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } + }); + } + + 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 = menu.findItem(R.id.action_create_contact); + MenuItem menuCreateConference = menu.findItem(R.id.action_join_conference); + MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline); + menuHideOffline.setChecked(this.mHideOfflineContacts); + mMenuSearchView = 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,null); + return true; + case R.id.action_join_conference: + showJoinConferenceDialog(null); + return true; + case R.id.action_scan_qr_code: + new IntentIntegrator(this).initiateScan(); + return true; + case R.id.action_hide_offline: + mHideOfflineContacts = !item.isChecked(); + getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit(); + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } + invalidateOptionsMenu(); + } + 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 + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null && scanResult.getFormatName() != null) { + String data = scanResult.getContents(); + Invite invite = new Invite(data); + if (xmppConnectionServiceBound) { + invite.invite(); + } else if (invite.getJid() != null) { + this.mPendingInvite = invite; + } else { + this.mPendingInvite = null; + } + } + } + super.onActivityResult(requestCode, requestCode, intent); + } + + @Override + protected void onBackendConnected() { + this.mActivatedAccounts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.State.DISABLED) { + this.mActivatedAccounts.add(account.getJid().toBareJid().toString()); + } + } + final Intent intent = getIntent(); + final ActionBar ab = getActionBar(); + if (intent != null && intent.getBooleanExtra("init",false) && ab != null) { + ab.setDisplayShowHomeEnabled(false); + ab.setDisplayHomeAsUpEnabled(false); + ab.setHomeButtonEnabled(false); + } + this.mKnownHosts = xmppConnectionService.getKnownHosts(); + this.mKnownConferenceHosts = xmppConnectionService.getKnownConferenceHosts(); + if (this.mPendingInvite != null) { + mPendingInvite.invite(); + this.mPendingInvite = null; + } else if (!handleIntent(getIntent())) { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } else { + filter(null); + } + } + setIntent(null); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + Invite getInviteJellyBean(NdefRecord record) { + return new Invite(record.toUri()); + } + + protected boolean handleIntent(Intent intent) { + if (intent == null || intent.getAction() == null) { + return false; + } + switch (intent.getAction()) { + case Intent.ACTION_SENDTO: + case Intent.ACTION_VIEW: + Log.d(Config.LOGTAG, "received uri=" + intent.getData()); + return new Invite(intent.getData()).invite(); + case NfcAdapter.ACTION_NDEF_DISCOVERED: + for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) { + if (message instanceof NdefMessage) { + Log.d(Config.LOGTAG, "received message=" + message); + for (NdefRecord record : ((NdefMessage) message).getRecords()) { + switch (record.getTnf()) { + case NdefRecord.TNF_WELL_KNOWN: + if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return getInviteJellyBean(record).invite(); + } else { + byte[] payload = record.getPayload(); + if (payload[0] == 0) { + return new Invite(Uri.parse(new String(Arrays.copyOfRange( + payload, 1, payload.length)))).invite(); + } + } + } + } + } + } + } + } + return false; + } + + private boolean handleJid(Invite invite) { + List contacts = xmppConnectionService.findContacts(invite.getJid()); + if (contacts.size() == 0) { + showCreateContactDialog(invite.getJid().toString(),invite.getFingerprint()); + return false; + } else if (contacts.size() == 1) { + Contact contact = contacts.get(0); + if (invite.getFingerprint() != null) { + if (contact.addOtrFingerprint(invite.getFingerprint())) { + Log.d(Config.LOGTAG,"added new fingerprint"); + xmppConnectionService.syncRosterToDisk(contact.getAccount()); + } + } + switchToConversation(contact); + return true; + } else { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(""); + mSearchEditText.append(invite.getJid().toString()); + filter(invite.getJid().toString()); + } else { + mInitialJid = invite.getJid().toString(); + } + 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.State.DISABLED) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && contact.match(needle) + && (!this.mHideOfflineContacts + || contact.getPresences().getMostAvailableStatus() < Presences.OFFLINE)) { + 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.State.DISABLED) { + for (Bookmark bookmark : account.getBookmarks()) { + if (bookmark.match(needle)) { + this.conferences.add(bookmark); + } + } + } + } + Collections.sort(this.conferences); + mConferenceAdapter.notifyDataSetChanged(); + } + + private void onTabChanged() { + invalidateOptionsMenu(); + } + + @Override + public void OnUpdateBlocklist(final Status status) { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } + } + + public static class MyListFragment extends ListFragment { + private AdapterView.OnItemClickListener mOnItemClickListener; + private int mResContextMenu; + + public void setContextMenu(final int res) { + this.mResContextMenu = res; + } + + @Override + public void onListItemClick(final ListView l, final View v, final int position, final long id) { + if (mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(l, v, position, id); + } + } + + public void setOnListItemClickListener(AdapterView.OnItemClickListener l) { + this.mOnItemClickListener = l; + } + + @Override + public void onViewCreated(final View view, final Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + registerForContextMenu(getListView()); + getListView().setFastScrollEnabled(true); + } + + @Override + public void onCreateContextMenu(final ContextMenu menu, final View v, + final ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + final StartConversationActivity activity = (StartConversationActivity) getActivity(); + activity.getMenuInflater().inflate(mResContextMenu, menu); + final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + if (mResContextMenu == R.menu.conference_context) { + activity.conference_context_id = acmi.position; + } else { + activity.contact_context_id = acmi.position; + final Blockable contact = (Contact) activity.contacts.get(acmi.position); + + final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock); + if (blockUnblockItem != null) { + if (contact.isBlocked()) { + blockUnblockItem.setTitle(R.string.unblock_contact); + } else { + blockUnblockItem.setTitle(R.string.block_contact); + } + } + } + } + + @Override + public boolean onContextItemSelected(final 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_contact_block_unblock: + activity.toggleContactBlock(); + 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; + } + } + + private class Invite extends XmppUri { + + public Invite(final Uri uri) { + super(uri); + } + + public Invite(final String uri) { + super(uri); + } + + boolean invite() { + if (jid != null) { + if (muc) { + showJoinConferenceDialog(jid); + } else { + return handleJid(this); + } + } + return false; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/TimePreference.java b/src/main/java/de/thedevstack/conversationsplus/ui/TimePreference.java new file mode 100644 index 00000000..bfe7c8f4 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/TimePreference.java @@ -0,0 +1,105 @@ +package de.thedevstack.conversationsplus.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.DialogPreference; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TimePicker; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; + +public class TimePreference extends DialogPreference implements Preference.OnPreferenceChangeListener { + private TimePicker picker = null; + public final static long DEFAULT_VALUE = 0; + + public TimePreference(final Context context, final AttributeSet attrs) { + super(context, attrs, 0); + this.setOnPreferenceChangeListener(this); + } + + protected void setTime(final long time) { + persistLong(time); + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } + + protected void updateSummary(final long time) { + final DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(getContext()); + final Date date = new Date(time); + setSummary(dateFormat.format(date.getTime())); + } + + @Override + protected View onCreateDialogView() { + picker = new TimePicker(getContext()); + picker.setIs24HourView(android.text.format.DateFormat.is24HourFormat(getContext())); + return picker; + } + + protected Calendar getPersistedTime() { + final Calendar c = Calendar.getInstance(); + c.setTimeInMillis(getPersistedLong(DEFAULT_VALUE)); + + return c; + } + + @SuppressWarnings("NullableProblems") + @Override + protected void onBindDialogView(final View v) { + super.onBindDialogView(v); + final Calendar c = getPersistedTime(); + + picker.setCurrentHour(c.get(Calendar.HOUR_OF_DAY)); + picker.setCurrentMinute(c.get(Calendar.MINUTE)); + } + + @Override + protected void onDialogClosed(final boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult) { + final Calendar c = Calendar.getInstance(); + c.set(Calendar.MINUTE, picker.getCurrentMinute()); + c.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour()); + + + if (!callChangeListener(c.getTimeInMillis())) { + return; + } + + setTime(c.getTimeInMillis()); + } + } + + @Override + protected Object onGetDefaultValue(final TypedArray a, final int index) { + return a.getInteger(index, 0); + } + + @Override + protected void onSetInitialValue(final boolean restorePersistedValue, final Object defaultValue) { + long time; + if (defaultValue == null) { + time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE; + } else if (defaultValue instanceof Long) { + time = restorePersistedValue ? getPersistedLong((Long) defaultValue) : (Long) defaultValue; + } else if (defaultValue instanceof Calendar) { + time = restorePersistedValue ? getPersistedLong(((Calendar)defaultValue).getTimeInMillis()) : ((Calendar)defaultValue).getTimeInMillis(); + } else { + time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE; + } + + setTime(time); + updateSummary(time); + } + + @Override + public boolean onPreferenceChange(final Preference preference, final Object newValue) { + ((TimePreference) preference).updateSummary((Long)newValue); + return true; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/UiCallback.java b/src/main/java/de/thedevstack/conversationsplus/ui/UiCallback.java new file mode 100644 index 00000000..0d23d29e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/UiCallback.java @@ -0,0 +1,11 @@ +package de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/ui/VerifyOTRActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/VerifyOTRActivity.java new file mode 100644 index 00000000..9f867dd2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/VerifyOTRActivity.java @@ -0,0 +1,446 @@ +package de.thedevstack.conversationsplus.ui; + +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.XmppUri; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { + + public static final String ACTION_VERIFY_CONTACT = "verify_contact"; + public static final int MODE_SCAN_FINGERPRINT = - 0x0502; + public static final int MODE_ASK_QUESTION = 0x0503; + public static final int MODE_ANSWER_QUESTION = 0x0504; + public static final int MODE_MANUAL_VERIFICATION = 0x0505; + + private LinearLayout mManualVerificationArea; + private LinearLayout mSmpVerificationArea; + private TextView mRemoteFingerprint; + private TextView mYourFingerprint; + private TextView mVerificationExplain; + private TextView mStatusMessage; + private TextView mSharedSecretHint; + private EditText mSharedSecretHintEditable; + private EditText mSharedSecretSecret; + private Button mLeftButton; + private Button mRightButton; + private Account mAccount; + private Conversation mConversation; + private int mode = MODE_MANUAL_VERIFICATION; + private XmppUri mPendingUri = null; + + private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialogInterface, int click) { + mConversation.verifyOtrFingerprint(); + xmppConnectionService.syncRosterToDisk(mConversation.getAccount()); + Toast.makeText(VerifyOTRActivity.this,R.string.verified,Toast.LENGTH_SHORT).show(); + finish(); + } + }; + + private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(final View view) { + if (isAccountOnline()) { + final String question = mSharedSecretHintEditable.getText().toString(); + final String secret = mSharedSecretSecret.getText().toString(); + if (question.trim().isEmpty()) { + mSharedSecretHintEditable.requestFocus(); + mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty)); + } else if (secret.trim().isEmpty()) { + mSharedSecretSecret.requestFocus(); + mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty)); + } else { + mSharedSecretSecret.setError(null); + mSharedSecretHintEditable.setError(null); + initSmp(question, secret); + updateView(); + } + } + } + }; + private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (isAccountOnline()) { + abortSmp(); + updateView(); + } + } + }; + private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() { + + @Override + public void onClick(View view) { + if (isAccountOnline()) { + final String question = mSharedSecretHintEditable.getText().toString(); + final String secret = mSharedSecretSecret.getText().toString(); + respondSmp(question, secret); + updateView(); + } + } + }; + private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + mConversation.smp().hint = null; + mConversation.smp().secret = null; + updateView(); + } + }; + private View.OnClickListener mFinishListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + finish(); + } + }; + + protected boolean initSmp(final String question, final String secret) { + final Session session = mConversation.getOtrSession(); + if (session!=null) { + try { + session.initSmp(question, secret); + mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED; + mConversation.smp().secret = secret; + mConversation.smp().hint = question; + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean abortSmp() { + final Session session = mConversation.getOtrSession(); + if (session!=null) { + try { + session.abortSmp(); + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + mConversation.smp().hint = null; + mConversation.smp().secret = null; + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean respondSmp(final String question, final String secret) { + final Session session = mConversation.getOtrSession(); + if (session!=null) { + try { + session.respondSmp(question,secret); + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean verifyWithUri(XmppUri uri) { + Contact contact = mConversation.getContact(); + if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) { + contact.addOtrFingerprint(uri.getFingerprint()); + Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show(); + updateView(); + xmppConnectionService.syncRosterToDisk(contact.getAccount()); + return true; + } else { + Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show(); + return false; + } + } + + protected boolean isAccountOnline() { + if (this.mAccount.getStatus() != Account.State.ONLINE) { + Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_SHORT).show(); + return false; + } else { + return true; + } + } + + protected boolean handleIntent(Intent intent) { + if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) { + try { + this.mAccount = this.xmppConnectionService.findAccountByJid(Jid.fromString(intent.getExtras().getString("account"))); + } catch (final InvalidJidException ignored) { + return false; + } + try { + this.mConversation = this.xmppConnectionService.find(this.mAccount,Jid.fromString(intent.getExtras().getString("contact"))); + if (this.mConversation == null) { + return false; + } + } catch (final InvalidJidException ignored) { + return false; + } + this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION); + if (this.mode == MODE_SCAN_FINGERPRINT) { + new IntentIntegrator(this).initiateScan(); + return false; + } + return true; + } else { + return false; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null && scanResult.getFormatName() != null) { + String data = scanResult.getContents(); + XmppUri uri = new XmppUri(data); + if (xmppConnectionServiceBound) { + verifyWithUri(uri); + finish(); + } else { + this.mPendingUri = uri; + } + } else { + finish(); + } + } + super.onActivityResult(requestCode, requestCode, intent); + } + + @Override + protected void onBackendConnected() { + if (handleIntent(getIntent())) { + updateView(); + } else if (mPendingUri!=null) { + verifyWithUri(mPendingUri); + finish(); + mPendingUri = null; + } + setIntent(null); + } + + protected void updateView() { + if (this.mConversation.hasValidOtrSession()) { + final ActionBar actionBar = getActionBar(); + this.mVerificationExplain.setText(R.string.no_otr_session_found); + invalidateOptionsMenu(); + switch(this.mode) { + case MODE_ASK_QUESTION: + if (actionBar != null ) { + actionBar.setTitle(R.string.ask_question); + } + this.updateViewAskQuestion(); + break; + case MODE_ANSWER_QUESTION: + if (actionBar != null ) { + actionBar.setTitle(R.string.smp_requested); + } + this.updateViewAnswerQuestion(); + break; + case MODE_MANUAL_VERIFICATION: + default: + if (actionBar != null ) { + actionBar.setTitle(R.string.manually_verify); + } + this.updateViewManualVerification(); + break; + } + } else { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.GONE); + } + } + + protected void updateViewManualVerification() { + this.mVerificationExplain.setText(R.string.manual_verification_explanation); + this.mManualVerificationArea.setVisibility(View.VISIBLE); + this.mSmpVerificationArea.setVisibility(View.GONE); + this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); + this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint())); + if (this.mConversation.isOtrFingerprintVerified()) { + deactivateButton(this.mRightButton,R.string.verified); + activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); + } else { + activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); + activateButton(this.mRightButton,R.string.verify, new View.OnClickListener() { + @Override + public void onClick(View view) { + showManuallyVerifyDialog(); + } + }); + } + } + + protected void updateViewAskQuestion() { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.VISIBLE); + this.mVerificationExplain.setText(R.string.smp_explain_question); + final int smpStatus = this.mConversation.smp().status; + switch (smpStatus) { + case Conversation.Smp.STATUS_WE_REQUESTED: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint); + this.mSharedSecretSecret.setText(this.mConversation.smp().secret); + this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener); + this.deactivateButton(this.mRightButton, R.string.in_progress); + break; + case Conversation.Smp.STATUS_FAILED: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.requestFocus(); + this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); + this.deactivateButton(this.mLeftButton, R.string.cancel); + this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener); + break; + case Conversation.Smp.STATUS_VERIFIED: + this.mSharedSecretHintEditable.setText(""); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretSecret.setText(""); + this.mSharedSecretSecret.setVisibility(View.GONE); + this.mStatusMessage.setVisibility(View.VISIBLE); + this.deactivateButton(this.mLeftButton, R.string.cancel); + this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); + break; + default: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); + this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener); + break; + } + } + + protected void updateViewAnswerQuestion() { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.VISIBLE); + this.mVerificationExplain.setText(R.string.smp_explain_answer); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretHint.setVisibility(View.VISIBLE); + this.deactivateButton(this.mLeftButton, R.string.cancel); + final int smpStatus = this.mConversation.smp().status; + switch (smpStatus) { + case Conversation.Smp.STATUS_CONTACT_REQUESTED: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHint.setText(this.mConversation.smp().hint); + this.activateButton(this.mRightButton,R.string.respond,this.mRespondSharedSecretListener); + break; + case Conversation.Smp.STATUS_VERIFIED: + this.mSharedSecretHintEditable.setText(""); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretHint.setVisibility(View.GONE); + this.mSharedSecretSecret.setText(""); + this.mSharedSecretSecret.setVisibility(View.GONE); + this.mStatusMessage.setVisibility(View.VISIBLE); + this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); + break; + case Conversation.Smp.STATUS_FAILED: + default: + this.mSharedSecretSecret.requestFocus(); + this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); + this.activateButton(this.mRightButton,R.string.finish,this.mFinishListener); + break; + } + } + + protected void activateButton(Button button, int text, View.OnClickListener listener) { + button.setEnabled(true); + button.setTextColor(getPrimaryTextColor()); + button.setText(text); + button.setOnClickListener(listener); + } + + protected void deactivateButton(Button button, int text) { + button.setEnabled(false); + button.setTextColor(getSecondaryTextColor()); + button.setText(text); + button.setOnClickListener(null); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_verify_otr); + this.mRemoteFingerprint = (TextView) findViewById(R.id.remote_fingerprint); + this.mYourFingerprint = (TextView) findViewById(R.id.your_fingerprint); + this.mLeftButton = (Button) findViewById(R.id.left_button); + this.mRightButton = (Button) findViewById(R.id.right_button); + this.mVerificationExplain = (TextView) findViewById(R.id.verification_explanation); + this.mStatusMessage = (TextView) findViewById(R.id.status_message); + this.mSharedSecretSecret = (EditText) findViewById(R.id.shared_secret_secret); + this.mSharedSecretHintEditable = (EditText) findViewById(R.id.shared_secret_hint_editable); + this.mSharedSecretHint = (TextView) findViewById(R.id.shared_secret_hint); + this.mManualVerificationArea = (LinearLayout) findViewById(R.id.manual_verification_area); + this.mSmpVerificationArea = (LinearLayout) findViewById(R.id.smp_verification_area); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.verify_otr, menu); + return true; + } + + private void showManuallyVerifyDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.manually_verify); + builder.setMessage(R.string.are_you_sure_verify_fingerprint); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener); + builder.create().show(); + } + + @Override + protected String getShareableUri() { + if (mAccount!=null) { + return mAccount.getShareableUri(); + } else { + return ""; + } + } + + public void onConversationUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + updateView(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java new file mode 100644 index 00000000..6c1f0439 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java @@ -0,0 +1,957 @@ +package de.thedevstack.conversationsplus.ui; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.NfcEvent; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.SystemClock; +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; +import android.widget.Toast; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import net.java.otr4j.session.SessionID; + +import java.io.FileNotFoundException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.entities.Presences; +import de.thedevstack.conversationsplus.services.AvatarService; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.services.XmppConnectionService.XmppConnectionBinder; +import de.thedevstack.conversationsplus.utils.ExceptionHelper; +import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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 registeredListeners = 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 int mTheme; + protected boolean mUsingEnterKey = false; + + private long mLastUiRefresh = 0; + private Handler mRefreshUiHandler = new Handler(); + private Runnable mRefreshUiRunnable = new Runnable() { + @Override + public void run() { + mLastUiRefresh = SystemClock.elapsedRealtime(); + refreshUiReal(); + } + }; + + + protected void refreshUi() { + final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; + if (diff > Config.REFRESH_UI_INTERVAL) { + mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); + runOnUiThread(mRefreshUiRunnable); + } else { + final long next = Config.REFRESH_UI_INTERVAL - diff; + mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); + mRefreshUiHandler.postDelayed(mRefreshUiRunnable,next); + } + } + + protected void refreshUiReal() { + + }; + + 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; + if (!registeredListeners && shouldRegisterListeners()) { + registerListeners(); + registeredListeners = true; + } + onBackendConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + xmppConnectionServiceBound = false; + } + }; + + @Override + protected void onStart() { + super.onStart(); + if (!xmppConnectionServiceBound) { + connectToBackend(); + } else { + if (!registeredListeners) { + this.registerListeners(); + this.registeredListeners = true; + } + this.onBackendConnected(); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected boolean shouldRegisterListeners() { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return !isDestroyed() && !isFinishing(); + } else { + return !isFinishing(); + } + } + + 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) { + if (registeredListeners) { + this.unregisterListeners(); + this.registeredListeners = false; + } + 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(); + + protected void registerListeners() { + if (this instanceof XmppConnectionService.OnConversationUpdate) { + this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); + } + if (this instanceof XmppConnectionService.OnAccountUpdate) { + this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); + } + if (this instanceof XmppConnectionService.OnRosterUpdate) { + this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); + } + if (this instanceof XmppConnectionService.OnMucRosterUpdate) { + this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); + } + if (this instanceof OnUpdateBlocklist) { + this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this); + } + } + + protected void unregisterListeners() { + if (this instanceof XmppConnectionService.OnConversationUpdate) { + this.xmppConnectionService.removeOnConversationListChangedListener(); + } + if (this instanceof XmppConnectionService.OnAccountUpdate) { + this.xmppConnectionService.removeOnAccountListChangedListener(); + } + if (this instanceof XmppConnectionService.OnRosterUpdate) { + this.xmppConnectionService.removeOnRosterUpdateListener(); + } + if (this instanceof XmppConnectionService.OnMucRosterUpdate) { + this.xmppConnectionService.removeOnMucRosterUpdateListener(); + } + if (this instanceof OnUpdateBlocklist) { + this.xmppConnectionService.removeOnUpdateBlocklistListener(); + } + } + + @Override + public boolean onOptionsItemSelected(final 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; + case R.id.action_show_qr_code: + showQrCode(); + 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); + this.mTheme = findTheme(); + setTheme(this.mTheme); + this.mUsingEnterKey = usingEnterKey(); + mUseSubject = getPreferences().getBoolean("use_subject", true); + + Settings.initSettingsClassWithPreferences(getPreferences()); + + final ActionBar ab = getActionBar(); + if (ab!=null) { + ab.setDisplayHomeAsUpEnabled(true); + } + } + + protected boolean usingEnterKey() { + return getPreferences().getBoolean("display_enter_key", false); + } + + 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) { + switchToConversation(conversation,text,null,newTask); + } + + public void highlightInMuc(Conversation conversation, String nick) { + switchToConversation(conversation,null,nick,false); + } + + private void switchToConversation(Conversation conversation, String text, String nick, 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); + } + if (nick != null) { + viewConversationIntent.putExtra(ConversationActivity.NICK, nick); + } + 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().toBareJid().toString()); + intent.putExtra("contact", contact.getJid().toString()); + startActivity(intent); + } + + public void switchToAccount(Account account) { + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().toBareJid().toString()); + startActivity(intent); + } + + protected void inviteToConversation(Conversation conversation) { + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + List contacts = new ArrayList<>(); + if (conversation.getMode() == Conversation.MODE_MULTI) { + for (MucOptions.User user : conversation.getMucOptions().getUsers()) { + Jid jid = user.getJid(); + if (jid != null) { + contacts.add(jid.toBareJid().toString()); + } + } + } else { + contacts.add(conversation.getJid().toBareJid().toString()); + } + intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()])); + intent.putExtra("conversation", conversation.getUuid()); + intent.putExtra("multiple", true); + 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 (final SendIntentException ignored) { + } + } + + @Override + public void success(Account account) { + xmppConnectionService.databaseBackend + .updateAccount(account); + xmppConnectionService.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) { + showAddToRosterDialog(conversation.getContact()); + } + + protected void showAddToRosterDialog(final Contact contact) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(contact.getJid().toString()); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + final Jid jid = contact.getJid(); + Account account = contact.getAccount(); + Contact contact = account.getRoster().getContact(jid); + xmppConnectionService.createContact(contact); + } + }); + builder.create().show(); + } + + private void showAskForPresenceDialog(final Contact contact) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(contact.getJid().toString()); + 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().toString()); + 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.setNextCounterpart(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 = 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) { + final Contact contact = conversation.getContact(); + if (conversation.hasValidOtrSession()) { + SessionID id = conversation.getOtrSession().getSessionID(); + Jid jid; + try { + jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID()); + } catch (InvalidJidException e) { + jid = null; + } + conversation.setNextCounterpart(jid); + listener.onPresenceSelected(); + } else 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.State.ONLINE) { + showAskForPresenceDialog(contact); + } else if (!contact.getOption(Contact.Options.TO) + || !contact.getOption(Contact.Options.FROM)) { + warnMutalPresenceSubscription(conversation, listener); + } else { + conversation.setNextCounterpart(null); + listener.onPresenceSelected(); + } + } else if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + try { + conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence)); + } catch (InvalidJidException e) { + conversation.setNextCounterpart(null); + } + 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) { + try { + conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString())); + } catch (InvalidJidException e) { + conversation.setNextCounterpart(null); + } + listener.onPresenceSelected(); + } + }); + builder.create().show(); + } + } + } + + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_INVITE_TO_CONVERSATION + && resultCode == RESULT_OK) { + try { + String conversationUuid = data.getStringExtra("conversation"); + Conversation conversation = xmppConnectionService + .findConversationByUuid(conversationUuid); + List jids = new ArrayList(); + if (data.getBooleanExtra("multiple", false)) { + String[] toAdd = data.getStringArrayExtra("contacts"); + for (String item : toAdd) { + jids.add(Jid.fromString(item)); + } + } else { + jids.add(Jid.fromString(data.getStringExtra("contact"))); + } + + if (conversation.getMode() == Conversation.MODE_MULTI) { + for (Jid jid : jids) { + xmppConnectionService.invite(conversation, jid); + } + } else { + jids.add(conversation.getJid().toBareJid()); + xmppConnectionService.createAdhocConference(conversation.getAccount(), jids, adhocCallback); + } + } catch (final InvalidJidException ignored) { + + } + } + } + + private UiCallback adhocCallback = new UiCallback() { + @Override + public void success(final Conversation conversation) { + switchToConversation(conversation); + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show(); + } + }); + } + + @Override + public void error(final int errorCode, Conversation object) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show(); + } + }); + } + + @Override + public void userInputRequried(PendingIntent pi, Conversation object) { + + } + }; + + 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 getOnlineColor() { + return this.mColorGreen; + } + + public int getSecondaryBackgroundColor() { + return this.mSecondaryBackgroundColor; + } + + public int getPixel(int dp) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + + public boolean copyTextToClipboard(String text, int labelResId) { + ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String label = getResources().getString(labelResId); + if (mClipBoardManager != null) { + ClipData mClipData = ClipData.newPlainText(label, text); + mClipBoardManager.setPrimaryClip(mClipData); + return true; + } + return false; + } + + protected void registerNdefPushMessageCallback() { + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter != null && nfcAdapter.isEnabled()) { + nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { + @Override + public NdefMessage createNdefMessage(NfcEvent nfcEvent) { + return new NdefMessage(new NdefRecord[]{ + NdefRecord.createUri(getShareableUri()), + NdefRecord.createApplicationRecord("eu.siacs.conversations") + }); + } + }, this); + } + } + + protected void unregisterNdefPushMessageCallback() { + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter != null && nfcAdapter.isEnabled()) { + nfcAdapter.setNdefPushMessageCallback(null,this); + } + } + + protected String getShareableUri() { + return null; + } + + @Override + public void onResume() { + super.onResume(); + if (this.getShareableUri()!=null) { + this.registerNdefPushMessageCallback(); + } + } + + protected int findTheme() { + if (getPreferences().getBoolean("use_larger_font", false)) { + return R.style.ConversationsTheme_LargerText; + } else { + return R.style.ConversationsTheme; + } + } + + @Override + public void onPause() { + super.onPause(); + this.unregisterNdefPushMessageCallback(); + } + + protected void showQrCode() { + String uri = getShareableUri(); + if (uri!=null) { + Point size = new Point(); + getWindowManager().getDefaultDisplay().getSize(size); + final int width = (size.x < size.y ? size.x : size.y); + Bitmap bitmap = createQrCodeBitmap(uri, width); + ImageView view = new ImageView(this); + view.setImageBitmap(bitmap); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setView(view); + builder.create().show(); + } + } + + protected Bitmap createQrCodeBitmap(String input, int size) { + Log.d(Config.LOGTAG,"qr code requested size: "+size); + try { + final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter(); + final Hashtable hints = new Hashtable<>(); + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); + final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints); + final int width = result.getWidth(); + final int height = result.getHeight(); + final int[] pixels = new int[width * height]; + for (int y = 0; y < height; y++) { + final int offset = y * width; + for (int x = 0; x < width; x++) { + pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT; + } + } + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Log.d(Config.LOGTAG,"output size: "+width+"x"+height); + bitmap.setPixels(pixels, 0, width, 0, 0, width, height); + return bitmap; + } catch (final WriterException e) { + return null; + } + } + + 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 (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 (final RejectedExecutionException ignored) { + } + } + } + } + + 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/src/main/java/de/thedevstack/conversationsplus/ui/adapter/AccountAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/AccountAdapter.java new file mode 100644 index 00000000..1dd855d9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/AccountAdapter.java @@ -0,0 +1,54 @@ +package de.thedevstack.conversationsplus.ui.adapter; + +import java.util.List; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.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 = inflater.inflate(R.layout.account_row, parent, false); + } + TextView jid = (TextView) view.findViewById(R.id.account_jid); + jid.setText(account.getJid().toBareJid().toString()); + 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))); + statusView.setText(getContext().getString(account.getStatus().getReadableId())); + switch (account.getStatus()) { + case ONLINE: + statusView.setTextColor(activity.getOnlineColor()); + break; + case DISABLED: + case CONNECTING: + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + default: + statusView.setTextColor(activity.getWarningTextColor()); + break; + } + return view; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java new file mode 100644 index 00000000..cad9975b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java @@ -0,0 +1,224 @@ +package de.thedevstack.conversationsplus.ui.adapter; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.Presences; +import de.thedevstack.conversationsplus.ui.ConversationActivity; +import de.thedevstack.conversationsplus.ui.XmppActivity; +import de.thedevstack.conversationsplus.utils.UIHelper; +import github.ankushsachdeva.emojicon.EmojiconTextView; + +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 = 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.getJid().toBareJid().toString()); + } + EmojiconTextView mLastMessage = (EmojiconTextView) view.findViewById(R.id.conversation_lastmsg); + TextView mTimestamp = (TextView) view.findViewById(R.id.conversation_lastupdate); + ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage); + + if (Settings.SHOW_ONLINE_STATUS && conversation != null && conversation.getAccount().getStatus() == Account.State.ONLINE) { + TextView status = (TextView) view.findViewById(R.id.status); + + String color = "#000000"; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + switch (conversation.getContact().getMostAvailableStatus()) { + case Presences.ONLINE: + case Presences.CHAT: + color = "#259B23"; + break; + case Presences.AWAY: + case Presences.XA: + color = "#FF9800"; + break; + case Presences.DND: + color = "#E51C23"; + break; + } + } else if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().online()) { + color = "#259B23"; + } + status.setBackgroundColor(Color.parseColor(color)); + } + + Message message = conversation.getLatestMessage(); + + if (!conversation.isRead()) { + convName.setTypeface(null, Typeface.BOLD); + } else { + convName.setTypeface(null, Typeface.NORMAL); + } + + if (message.getImageParams().width > 0 + && (message.getDownloadable() == null + || message.getDownloadable().getStatus() != Downloadable.STATUS_DELETED)) { + mLastMessage.setVisibility(View.GONE); + imagePreview.setVisibility(View.VISIBLE); + activity.loadBitmap(message, imagePreview); + } else { + Pair preview = UIHelper.getMessagePreview(activity,message); + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + boolean parseEmoticons = Settings.PARSE_EMOTICONS; + CharSequence msgText = parseEmoticons ? UIHelper.transformAsciiEmoticons(getContext(), preview.first) : preview.first; + mLastMessage.setText(msgText); + if (preview.second) { + if (conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.ITALIC); + } else { + mLastMessage.setTypeface(null,Typeface.BOLD_ITALIC); + } + } else { + if (conversation.isRead()) { + mLastMessage.setTypeface(null,Typeface.NORMAL); + } else { + mLastMessage.setTypeface(null,Typeface.BOLD); + } + } + } + + mTimestamp.setText(UIHelper.readableTimeDifference(activity,conversation.getLatestMessage().getTimeSent())); + ImageView profilePicture = (ImageView) view.findViewById(R.id.conversation_image); + loadAvatar(conversation,profilePicture); + + return view; + } + + class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Conversation conversation = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(Conversation... params) { + return activity.avatarService().get(params[0], activity.getPixel(56)); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + public void loadAvatar(Conversation conversation, ImageView imageView) { + if (cancelPotentialWork(conversation, imageView)) { + final Bitmap bm = activity.avatarService().get(conversation, activity.getPixel(56), true); + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + imageView.setBackgroundColor(UIHelper.getColorForName(conversation.getName())); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(conversation); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + public static boolean cancelPotentialWork(Conversation conversation, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Conversation oldConversation = bitmapWorkerTask.conversation; + if (oldConversation == null || conversation != oldConversation) { + 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/KnownHostsAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/KnownHostsAdapter.java new file mode 100644 index 00000000..7bca0aa6 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/KnownHostsAdapter.java @@ -0,0 +1,71 @@ +package de.thedevstack.conversationsplus.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, new ArrayList()); + domains = new ArrayList(mKnownHosts); + } + + @Override + public Filter getFilter() { + return domainFilter; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java new file mode 100644 index 00000000..e78d7f5c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java @@ -0,0 +1,181 @@ +package de.thedevstack.conversationsplus.ui.adapter; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.ListItem; +import de.thedevstack.conversationsplus.ui.XmppActivity; +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class ListItemAdapter extends ArrayAdapter { + + protected XmppActivity activity; + protected boolean showDynamicTags = false; + private View.OnClickListener onTagTvClick = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (view instanceof TextView && mOnTagClickedListener != null) { + TextView tv = (TextView) view; + final String tag = tv.getText().toString(); + mOnTagClickedListener.onTagClicked(tag); + } + } + }; + private OnTagClickedListener mOnTagClickedListener = null; + + public ListItemAdapter(XmppActivity activity, List objects) { + super(activity, 0, objects); + this.activity = activity; + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false); + } + + @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 = inflater.inflate(R.layout.contact, parent, false); + } + TextView tvName = (TextView) view.findViewById(R.id.contact_display_name); + TextView tvJid = (TextView) view.findViewById(R.id.contact_jid); + ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); + LinearLayout tagLayout = (LinearLayout) view.findViewById(R.id.tags); + + List tags = item.getTags(); + if (tags.size() == 0 || !this.showDynamicTags) { + tagLayout.setVisibility(View.GONE); + } else { + tagLayout.setVisibility(View.VISIBLE); + tagLayout.removeAllViewsInLayout(); + for(ListItem.Tag tag : tags) { + TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tagLayout,false); + tv.setText(tag.getName()); + tv.setBackgroundColor(tag.getColor()); + tv.setOnClickListener(this.onTagTvClick); + tagLayout.addView(tv); + } + } + final Jid jid = item.getJid(); + if (jid != null) { + tvJid.setText(jid.toString()); + } else { + tvJid.setText(""); + } + tvName.setText(item.getDisplayName()); + loadAvatar(item,picture); + return view; + } + + public void setOnTagClickedListener(OnTagClickedListener listener) { + this.mOnTagClickedListener = listener; + } + + public interface OnTagClickedListener { + public void onTagClicked(String tag); + } + + class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private ListItem item = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(ListItem... params) { + return activity.avatarService().get(params[0], activity.getPixel(48)); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + public void loadAvatar(ListItem item, ImageView imageView) { + if (cancelPotentialWork(item, imageView)) { + final Bitmap bm = activity.avatarService().get(item,activity.getPixel(48),true); + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + imageView.setBackgroundColor(UIHelper.getColorForName(item.getDisplayName())); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(item); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + public static boolean cancelPotentialWork(ListItem item, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final ListItem oldItem = bitmapWorkerTask.item; + if (oldItem == null || item != oldItem) { + 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/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java new file mode 100644 index 00000000..19b993cc --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java @@ -0,0 +1,603 @@ +package de.thedevstack.conversationsplus.ui.adapter; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Typeface; +import android.net.Uri; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.List; + +import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.Message.ImageParams; +import de.thedevstack.conversationsplus.ui.ConversationActivity; +import de.thedevstack.conversationsplus.utils.GeoHelper; +import de.thedevstack.conversationsplus.utils.UIHelper; +import github.ankushsachdeva.emojicon.EmojiconTextView; + +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; + + private OnLongClickListener openContextMenu = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + v.showContextMenu(); + return true; + } + }; + + 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.getType() == Message.TYPE_FILE || message.getDownloadable() != null) { + ImageParams params = message.getImageParams(); + if (params.size > (1.5 * 1024 * 1024)) { + filesize = params.size / (1024 * 1024)+ " MiB"; + } else if (params.size > 0) { + filesize = params.size / 1024 + " KiB"; + } + if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) { + error = true; + } + } + switch (message.getMergedStatus()) { + case Message.STATUS_WAITING: + info = getContext().getString(R.string.waiting); + break; + case Message.STATUS_UNSEND: + Downloadable d = message.getDownloadable(); + if (d!=null) { + info = getContext().getString(R.string.sending_file,d.getProgress()); + } else { + info = getContext().getString(R.string.sending); + } + break; + case Message.STATUS_OFFERED: + info = getContext().getString(R.string.offering); + break; + case Message.STATUS_SEND_RECEIVED: + 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; + default: + if (multiReceived) { + info = UIHelper.getMessageDisplayName(message); + } + 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, String text) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(text); + 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(final ViewHolder viewHolder, final 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) { + final String nick = UIHelper.getMessageDisplayName(message); + final String formattedBody = message.getMergedBody().replaceAll("^" + Message.ME_COMMAND, + nick + " "); + if (message.getType() != Message.TYPE_PRIVATE) { + + if (message.hasMeCommand()) { + final Spannable span = new SpannableString(formattedBody); + span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(span); + } else { + boolean parseEmoticons = Settings.PARSE_EMOTICONS; + viewHolder.messageBody.setText(parseEmoticons ? UIHelper + .transformAsciiEmoticons(getContext(), message.getMergedBody()) + : message.getMergedBody()); + } + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity + .getString(R.string.private_message); + } else { + final String to; + if (message.getCounterpart() != null) { + to = message.getCounterpart().getResourcepart(); + } else { + to = ""; + } + privateMarker = activity.getString(R.string.private_message_to, to); + } + final Spannable span = new SpannableString(privateMarker + " " + + formattedBody); + span.setSpan(new ForegroundColorSpan(activity + .getSecondaryTextColor()), 0, privateMarker + .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan(new StyleSpan(Typeface.BOLD), 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (message.hasMeCommand()) { + span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarker.length() + 1, + privateMarker.length() + 1 + nick.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, String text) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(text); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + startDownloadable(message); + } + }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); + } + + private void displayOpenableMessage(ViewHolder viewHolder,final Message message) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity,message))); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + openDownloadable(message); + } + }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); + } + + private void displayLocationMessage(ViewHolder viewHolder, final Message message) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(R.string.show_location); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + showLocation(message); + } + }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); + } + + 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(openContextMenu); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + final Message message = getItem(position); + final Conversation conversation = message.getConversation(); + final Account account = conversation.getAccount(); + final int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case NULL: + view = activity.getLayoutInflater().inflate( + R.layout.message_null, parent, false); + break; + case SENT: + 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.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 = (EmojiconTextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + viewHolder.indicatorReceived = (ImageView) view + .findViewById(R.id.indicator_received); + break; + case RECEIVED: + 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); + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (EmojiconTextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + viewHolder.indicatorReceived = (ImageView) view + .findViewById(R.id.indicator_received); + break; + case STATUS: + view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); + viewHolder.contact_picture = (ImageView) view.findViewById(R.id.message_photo); + viewHolder.status_message = (TextView) view.findViewById(R.id.status_message); + break; + default: + viewHolder = null; + break; + } + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) view.getTag(); + if (viewHolder == null) { + return view; + } + } + + if (type == STATUS) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(conversation.getContact(), + activity.getPixel(32))); + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.status_message.setText(message.getBody()); + } + return view; + } else if (type == NULL) { + if (viewHolder.message_box != null) { + Log.e(Config.LOGTAG, "detected type=NULL but with wrong cached view"); + view = activity.getLayoutInflater().inflate(R.layout.message_null, parent, false); + view.setTag(new ViewHolder()); + } + if (position == getCount() - 1) { + view.getLayoutParams().height = 1; + } else { + view.getLayoutParams().height = 0; + + } + view.setLayoutParams(view.getLayoutParams()); + return view; + } else if (message.wasMergedIntoPrevious()) { + Log.e(Config.LOGTAG,"detected wasMergedIntoPrevious with wrong type"); + return view; + } else if (viewHolder.messageBody == null || viewHolder.image == null) { + return view; //avoiding weird platform bugs + } else if (type == RECEIVED) { + Contact contact = message.getContact(); + if (contact != null) { + viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(contact, activity.getPixel(48))); + } else if (conversation.getMode() == Conversation.MODE_MULTI) { + viewHolder.contact_picture.setImageBitmap(activity.avatarService().get( + UIHelper.getMessageDisplayName(message), + activity.getPixel(48))); + } + } else if (type == SENT) { + viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48))); + } + + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(message); + } + + } + }); + viewHolder.contact_picture + .setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(message); + return true; + } else { + return false; + } + } + }); + + final Downloadable downloadable = message.getDownloadable(); + if (downloadable != null && downloadable.getStatus() != Downloadable.STATUS_UPLOADING) { + if (downloadable.getStatus() == Downloadable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder,message,activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message))); + } else if (downloadable.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_image_filesize)); + } else { + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first); + } + } else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + displayImageMessage(viewHolder, message); + } else if (message.getType() == Message.TYPE_FILE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + if (message.getImageParams().width > 0) { + displayImageMessage(viewHolder,message); + } else { + displayOpenableMessage(viewHolder, message); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + if (activity.hasPgp()) { + displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message)); + } else { + displayInfoMessage(viewHolder, + activity.getString(R.string.install_openkeychain)); + if (viewHolder != null) { + viewHolder.message_box + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.showInstallPgpDialog(); + } + }); + } + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayDecryptionFailed(viewHolder); + } else { + if (GeoHelper.isGeoUri(message.getBody())) { + displayLocationMessage(viewHolder,message); + } else { + displayTextMessage(viewHolder, message); + } + } + + displayStatus(viewHolder, message); + + return view; + } + + public void startDownloadable(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(); + } + } + } + + public void openDownloadable(Message message) { + DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + if (!file.exists()) { + Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); + return; + } + Intent openIntent = new Intent(Intent.ACTION_VIEW); + openIntent.setDataAndType(Uri.fromFile(file), file.getMimeType()); + PackageManager manager = activity.getPackageManager(); + List infos = manager.queryIntentActivities(openIntent, 0); + if (infos.size() > 0) { + getContext().startActivity(openIntent); + } else { + Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show(); + } + } + + public void showLocation(Message message) { + for(Intent intent : GeoHelper.createGeoIntentsFromMessage(message)) { + if (intent.resolveActivity(getContext().getPackageManager()) != null) { + getContext().startActivity(intent); + return; + } + } + Toast.makeText(activity,R.string.no_application_found_to_display_location,Toast.LENGTH_SHORT).show(); + } + + public interface OnContactPictureClicked { + public void onContactPictureClicked(Message message); + } + + public interface OnContactPictureLongClicked { + public void onContactPictureLongClicked(Message message); + } + + 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 EmojiconTextView messageBody; + protected ImageView contact_picture; + protected TextView status_message; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/CryptoHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/CryptoHelper.java new file mode 100644 index 00000000..115ac190 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/CryptoHelper.java @@ -0,0 +1,124 @@ +package de.thedevstack.conversationsplus.utils; + +import java.security.SecureRandom; +import java.text.Normalizer; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; + +public final class CryptoHelper { + public static final String FILETRANSFER = "?FILETRANSFERv1:"; + private final static char[] hexArray = "0123456789abcdef".toCharArray(); + private final static char[] vowels = "aeiou".toCharArray(); + private final static char[] consonants = "bcdfghjklmnpqrstvwxyz".toCharArray(); + final public static byte[] ONE = new byte[] { 0, 0, 0, 1 }; + + 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 hexToString(final String hexString) { + return new String(hexToBytes(hexString)); + } + + public 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 randomMucName(SecureRandom random) { + return randomWord(3, random) + "." + randomWord(7, random); + } + + private 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(); + } + + /** + * Escapes usernames or passwords for SASL. + */ + public static String saslEscape(final String s) { + final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1)); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case ',': + sb.append("=2C"); + break; + case '=': + sb.append("=3D"); + break; + default: + sb.append(c); + break; + } + } + return sb.toString(); + } + + public static String saslPrep(final String s) { + return Normalizer.normalize(s, Normalizer.Form.NFKC); + } + + public static String prettifyFingerprint(String fingerprint) { + StringBuilder builder = new StringBuilder(fingerprint); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + return builder.toString(); + } + + public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) { + final Collection cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS)); + final List platformCiphers = Arrays.asList(platformSupportedCipherSuites); + cipherSuites.retainAll(platformCiphers); + cipherSuites.addAll(platformCiphers); + filterWeakCipherSuites(cipherSuites); + return cipherSuites.toArray(new String[cipherSuites.size()]); + } + + private static void filterWeakCipherSuites(final Collection cipherSuites) { + final Iterator it = cipherSuites.iterator(); + while (it.hasNext()) { + String cipherName = it.next(); + // remove all ciphers with no or very weak encryption or no authentication + for (String weakCipherPattern : Config.WEAK_CIPHER_PATTERNS) { + if (cipherName.contains(weakCipherPattern)) { + it.remove(); + break; + } + } + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java new file mode 100644 index 00000000..1eedfdfd --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java @@ -0,0 +1,183 @@ +package de.thedevstack.conversationsplus.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 de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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(final Jid jid) throws IOException { + final String host = jid.getDomainpart(); + String dns[] = client.findDNS(); + + if (dns != null) { + for (String dnsserver : dns) { + InetAddress ip = InetAddress.getByName(dnsserver); + Bundle b = queryDNS(host, ip); + if (b.containsKey("values")) { + 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 bundle = 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) { + bundle.putString("error", "nosrv"); + return bundle; + } + ArrayList values = new ArrayList<>(); + for (SRV srv : result) { + if (ips6.containsKey(srv.getName())) { + values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6)); + } + if (ips4.containsKey(srv.getName())) { + values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4)); + } + values.add(createNamePortBundle(srv.getName(),srv.getPort(),null)); + } + bundle.putParcelableArrayList("values", values); + } catch (SocketTimeoutException e) { + bundle.putString("error", "timeout"); + } catch (Exception e) { + bundle.putString("error", "unhandled"); + } + return bundle; + } + + private static Bundle createNamePortBundle(String name, int port, TreeMap> ips) { + Bundle namePort = new Bundle(); + namePort.putString("name", name); + namePort.putInt("port", port); + if (ips!=null) { + ArrayList ip = ips.get(name); + Collections.shuffle(ip, new Random()); + namePort.putString("ip", ip.get(0)); + } + 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/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHandler.java b/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHandler.java new file mode 100644 index 00000000..256287f1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHandler.java @@ -0,0 +1,46 @@ +package de.thedevstack.conversationsplus.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()); + os.flush(); + os.close(); + } 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/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java new file mode 100644 index 00000000..492c65d2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java @@ -0,0 +1,119 @@ +package de.thedevstack.conversationsplus.utils; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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().toBareJid() + + " to send in stack trace"); + Conversation conversation = null; + try { + conversation = service.findOrCreateConversation(finalAccount, + Jid.fromString("bugs@siacs.eu"), false); + } catch (final InvalidJidException ignored) { + } + 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) + .apply(); + } + }); + builder.create().show(); + } catch (final IOException ignored) { + } + + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ExifHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/ExifHelper.java new file mode 100644 index 00000000..13aff60d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ExifHelper.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.thedevstack.conversationsplus.utils; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; + +public class ExifHelper { + private static final String TAG = "CameraExif"; + + public static int getOrientation(InputStream is) { + if (is == null) { + return 0; + } + + byte[] buf = new byte[8]; + int length = 0; + + // ISO/IEC 10918-1:1993(E) + while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) { + int marker = buf[1] & 0xFF; + + // Check if the marker is a padding. + if (marker == 0xFF) { + continue; + } + + // Check if the marker is SOI or TEM. + if (marker == 0xD8 || marker == 0x01) { + continue; + } + // Check if the marker is EOI or SOS. + if (marker == 0xD9 || marker == 0xDA) { + return 0; + } + + // Get the length and check if it is reasonable. + if (!read(is, buf, 2)) { + return 0; + } + length = pack(buf, 0, 2, false); + if (length < 2) { + Log.e(TAG, "Invalid length"); + return 0; + } + length -= 2; + + // Break if the marker is EXIF in APP1. + if (marker == 0xE1 && length >= 6) { + if (!read(is, buf, 6)) return 0; + length -= 6; + if (pack(buf, 0, 4, false) == 0x45786966 && + pack(buf, 4, 2, false) == 0) { + break; + } + } + + // Skip other markers. + try { + is.skip(length); + } catch (IOException ex) { + return 0; + } + length = 0; + } + + // JEITA CP-3451 Exif Version 2.2 + if (length > 8) { + int offset = 0; + byte[] jpeg = new byte[length]; + if (!read(is, jpeg, length)) { + return 0; + } + + // Identify the byte order. + int tag = pack(jpeg, offset, 4, false); + if (tag != 0x49492A00 && tag != 0x4D4D002A) { + Log.e(TAG, "Invalid byte order"); + return 0; + } + boolean littleEndian = (tag == 0x49492A00); + + // Get the offset and check if it is reasonable. + int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; + if (count < 10 || count > length) { + Log.e(TAG, "Invalid offset"); + return 0; + } + offset += count; + length -= count; + + // Get the count and go through all the elements. + count = pack(jpeg, offset - 2, 2, littleEndian); + while (count-- > 0 && length >= 12) { + // Get the tag and check if it is orientation. + tag = pack(jpeg, offset, 2, littleEndian); + if (tag == 0x0112) { + // We do not really care about type and count, do we? + int orientation = pack(jpeg, offset + 8, 2, littleEndian); + switch (orientation) { + case 1: + return 0; + case 3: + return 180; + case 6: + return 90; + case 8: + return 270; + } + Log.i(TAG, "Unsupported orientation"); + return 0; + } + offset += 12; + length -= 12; + } + } + + Log.i(TAG, "Orientation not found"); + return 0; + } + + private static int pack(byte[] bytes, int offset, int length, + boolean littleEndian) { + int step = 1; + if (littleEndian) { + offset += length - 1; + step = -1; + } + + int value = 0; + while (length-- > 0) { + value = (value << 8) | (bytes[offset] & 0xFF); + offset += step; + } + return value; + } + + private static boolean read(InputStream is, byte[] buf, int length) { + try { + return is.read(buf, 0, length) == length; + } catch (IOException ex) { + return false; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java new file mode 100644 index 00000000..87c16f8c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java @@ -0,0 +1,71 @@ +package de.thedevstack.conversationsplus.utils; + +import android.content.Intent; +import android.net.Uri; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; + +public class GeoHelper { + private static Pattern GEO_URI = Pattern.compile("geo:([\\-0-9.]+),([\\-0-9.]+)(?:,([\\-0-9.]+))?(?:\\?(.*))?", Pattern.CASE_INSENSITIVE); + + public static boolean isGeoUri(String body) { + return body != null && GEO_URI.matcher(body).matches(); + } + + public static ArrayList createGeoIntentsFromMessage(Message message) { + final ArrayList intents = new ArrayList(); + Matcher matcher = GEO_URI.matcher(message.getBody()); + if (!matcher.matches()) { + return intents; + } + double latitude; + double longitude; + try { + latitude = Double.parseDouble(matcher.group(1)); + if (latitude > 90.0 || latitude < -90.0) { + return intents; + } + longitude = Double.parseDouble(matcher.group(2)); + if (longitude > 180.0 || longitude < -180.0) { + return intents; + } + } catch (NumberFormatException nfe) { + return intents; + } + final Conversation conversation = message.getConversation(); + String label; + if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { + try { + label = "(" + URLEncoder.encode(message.getConversation().getName(), "UTF-8") + ")"; + } catch (UnsupportedEncodingException e) { + label = ""; + } + } else { + label = ""; + } + + Intent locationPluginIntent = new Intent("eu.siacs.conversations.location.show"); + locationPluginIntent.putExtra("latitude",latitude); + locationPluginIntent.putExtra("longitude",longitude); + if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { + locationPluginIntent.putExtra("name",conversation.getName()); + } + intents.add(locationPluginIntent); + + Intent geoIntent = new Intent(Intent.ACTION_VIEW); + geoIntent.setData(Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude) + "?q=" + String.valueOf(latitude) + "," + String.valueOf(longitude) + label)); + intents.add(geoIntent); + + Intent httpIntent = new Intent(Intent.ACTION_VIEW); + httpIntent.setData(Uri.parse("https://maps.google.com/maps?q=loc:"+String.valueOf(latitude) + "," + String.valueOf(longitude) +label)); + intents.add(httpIntent); + return intents; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/OnPhoneContactsLoadedListener.java b/src/main/java/de/thedevstack/conversationsplus/utils/OnPhoneContactsLoadedListener.java new file mode 100644 index 00000000..e701d62b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/OnPhoneContactsLoadedListener.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.utils; + +import java.util.List; + +import android.os.Bundle; + +public interface OnPhoneContactsLoadedListener { + public void onPhoneContactsLoaded(List phoneContacts); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/PRNGFixes.java b/src/main/java/de/thedevstack/conversationsplus/utils/PRNGFixes.java new file mode 100644 index 00000000..78776aeb --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/PRNGFixes.java @@ -0,0 +1,327 @@ +package de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/utils/PhoneHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/PhoneHelper.java new file mode 100644 index 00000000..b49e2183 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/PhoneHelper.java @@ -0,0 +1,107 @@ +package de.thedevstack.conversationsplus.utils; + +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.content.pm.PackageManager; +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 List phoneContacts, final OnPhoneContactsLoadedListener listener) { + 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); + } + cursor.close(); + } + }); + 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); + mProfileCursor.close(); + if (uri == null) { + return null; + } else { + return Uri.parse(uri); + } + } + } + + public static String getVersionName(Context context) { + final String packageName = context == null ? null : context.getPackageName(); + if (packageName != null) { + try { + return context.getPackageManager().getPackageInfo(packageName, 0).versionName; + } catch (final PackageManager.NameNotFoundException e) { + return "unknown"; + } + } else { + return "unknown"; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java new file mode 100644 index 00000000..bda3f770 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java @@ -0,0 +1,366 @@ +package de.thedevstack.conversationsplus.utils; + +import java.net.URLConnection; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.text.Spannable.Factory; +import android.text.style.ImageSpan; +import android.text.Spannable; +import android.util.Pair; + +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) { + return sameDay(date,new Date(System.currentTimeMillis())); + } + + public static boolean sameDay(long timestamp1, long timestamp2) { + return sameDay(new Date(timestamp1),new Date(timestamp2)); + } + + private static boolean sameDay(Date a, Date b) { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(a); + cal2.setTime(b); + 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 final Map ANDROID_EMOTICONS = new HashMap(); + + private static final Factory spannableFactory = Spannable.Factory + .getInstance(); + + static { + addPattern(ANDROID_EMOTICONS, ":)", R.drawable.emo_im_happy); + addPattern(ANDROID_EMOTICONS, ":-)", R.drawable.emo_im_happy); + addPattern(ANDROID_EMOTICONS, ":(", R.drawable.emo_im_sad); + addPattern(ANDROID_EMOTICONS, ":-(", R.drawable.emo_im_sad); + addPattern(ANDROID_EMOTICONS, ";)", R.drawable.emo_im_winking); + addPattern(ANDROID_EMOTICONS, ";-)", R.drawable.emo_im_winking); + addPattern(ANDROID_EMOTICONS, ":P", + R.drawable.emo_im_tongue_sticking_out); + addPattern(ANDROID_EMOTICONS, ":-P", + R.drawable.emo_im_tongue_sticking_out); + addPattern(ANDROID_EMOTICONS, "=-O", R.drawable.emo_im_surprised); + addPattern(ANDROID_EMOTICONS, ":*", R.drawable.emo_im_kissing); + addPattern(ANDROID_EMOTICONS, ":-*", R.drawable.emo_im_kissing); + addPattern(ANDROID_EMOTICONS, ":O", R.drawable.emo_im_wtf); + addPattern(ANDROID_EMOTICONS, ":-O", R.drawable.emo_im_wtf); + addPattern(ANDROID_EMOTICONS, "B)", R.drawable.emo_im_cool); + addPattern(ANDROID_EMOTICONS, "B-)", R.drawable.emo_im_cool); + addPattern(ANDROID_EMOTICONS, "8)", R.drawable.emo_im_cool); + addPattern(ANDROID_EMOTICONS, "8-)", R.drawable.emo_im_cool); + addPattern(ANDROID_EMOTICONS, ":$", R.drawable.emo_im_money_mouth); + addPattern(ANDROID_EMOTICONS, ":-$", R.drawable.emo_im_money_mouth); + addPattern(ANDROID_EMOTICONS, ":-!", R.drawable.emo_im_foot_in_mouth); + addPattern(ANDROID_EMOTICONS, ":-[", R.drawable.emo_im_embarrassed); + addPattern(ANDROID_EMOTICONS, "O:)", R.drawable.emo_im_angel); + addPattern(ANDROID_EMOTICONS, "O:-)", R.drawable.emo_im_angel); + addPattern(ANDROID_EMOTICONS, ":\\", R.drawable.emo_im_undecided); + addPattern(ANDROID_EMOTICONS, ":-\\", R.drawable.emo_im_undecided); + addPattern(ANDROID_EMOTICONS, ":'(", R.drawable.emo_im_crying); + addPattern(ANDROID_EMOTICONS, ":D", R.drawable.emo_im_laughing); + addPattern(ANDROID_EMOTICONS, ":-D", R.drawable.emo_im_laughing); + addPattern(ANDROID_EMOTICONS, "O_o", R.drawable.emo_im_wtf); + addPattern(ANDROID_EMOTICONS, "o_O", R.drawable.emo_im_wtf); + addPattern(ANDROID_EMOTICONS, ">:O", R.drawable.emo_im_yelling); + addPattern(ANDROID_EMOTICONS, ">:0", R.drawable.emo_im_yelling); + addPattern(ANDROID_EMOTICONS, ":S", R.drawable.emo_im_lips_are_sealed); + addPattern(ANDROID_EMOTICONS, ":-S", R.drawable.emo_im_lips_are_sealed); + addPattern(ANDROID_EMOTICONS, "<3", R.drawable.emo_im_heart); + } + + private static void addPattern(Map map, String smile, + int resource) { + map.put(Pattern.compile(Pattern.quote(smile)), resource); + } + + private static boolean getSmiledText(Context context, Spannable spannable) { + boolean hasChanges = false; + Map emoticons = ANDROID_EMOTICONS; + for (Entry entry : emoticons.entrySet()) { + Matcher matcher = entry.getKey().matcher(spannable); + while (matcher.find()) { + boolean set = true; + for (ImageSpan span : spannable.getSpans(matcher.start(), + matcher.end(), ImageSpan.class)) + if (spannable.getSpanStart(span) >= matcher.start() + && spannable.getSpanEnd(span) <= matcher.end()) + spannable.removeSpan(span); + else { + set = false; + break; + } + if (set) { + spannable.setSpan(new ImageSpan(context, entry.getValue()), + matcher.start(), matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + hasChanges = true; + } + } + } + return hasChanges; + } + + 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 transformAsciiEmoticonsToUtf8(String body) { + if (body != null) { + for (EmoticonPattern p : patterns) { + body = p.replaceAll(body); + } + } + return body; + } + + public static Spannable transformAsciiEmoticons(Context context, String body) { + Spannable spannable; + if (Config.UTF8_EMOTICONS) { + spannable = spannableFactory.newSpannable(transformAsciiEmoticonsToUtf8(body)); + } + else { + spannable = spannableFactory.newSpannable(body); + getSmiledText(context, spannable); + } + return spannable; + } + + public static int getColorForName(String name) { + if (name.isEmpty()) { + return 0xFF202020; + } + int colors[] = {0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5, + 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722, + 0xFF795548, 0xFF607d8b}; + return colors[(int) ((name.hashCode() & 0xffffffffl) % colors.length)]; + } + + public static Pair getMessagePreview(final Context context, final Message message) { + final Downloadable d = message.getDownloadable(); + if (d != null ) { + switch (d.getStatus()) { + case Downloadable.STATUS_CHECKING: + return new Pair<>(context.getString(R.string.checking_image),true); + case Downloadable.STATUS_DOWNLOADING: + return new Pair<>(context.getString(R.string.receiving_x_file, + getFileDescriptionString(context,message), + d.getProgress()),true); + case Downloadable.STATUS_OFFER: + case Downloadable.STATUS_OFFER_CHECK_FILESIZE: + return new Pair<>(context.getString(R.string.x_file_offered_for_download, + getFileDescriptionString(context,message)),true); + case Downloadable.STATUS_DELETED: + return new Pair<>(context.getString(R.string.file_deleted),true); + case Downloadable.STATUS_FAILED: + return new Pair<>(context.getString(R.string.file_transmission_failed),true); + case Downloadable.STATUS_UPLOADING: + if (message.getStatus() == Message.STATUS_OFFERED) { + return new Pair<>(context.getString(R.string.offering_x_file, + getFileDescriptionString(context, message)), true); + } else { + return new Pair<>(context.getString(R.string.sending_x_file, + getFileDescriptionString(context, message)), true); + } + default: + return new Pair<>("",false); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + return new Pair<>(context.getString(R.string.encrypted_message_received),true); + } else if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + return new Pair<>(context.getString(R.string.received_x_file, + getFileDescriptionString(context, message)), true); + } else { + return new Pair<>(getFileDescriptionString(context,message),true); + } + } else { + if (message.getBody().startsWith(Message.ME_COMMAND)) { + return new Pair<>(message.getBody().replaceAll("^" + Message.ME_COMMAND, + UIHelper.getMessageDisplayName(message) + " "), false); + } else if (GeoHelper.isGeoUri(message.getBody())) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + return new Pair<>(context.getString(R.string.received_location),true); + } else { + return new Pair<>(context.getString(R.string.location), true); + } + } else{ + return new Pair<>(message.getBody(), false); + } + } + } + + public static String getFileDescriptionString(final Context context, final Message message) { + if (message.getType() == Message.TYPE_IMAGE) { + return context.getString(R.string.image); + } + final String path = message.getRelativeFilePath(); + if (path == null) { + return ""; + } + final String mime; + try { + mime = URLConnection.guessContentTypeFromName(path.replace("#","")); + } catch (final StringIndexOutOfBoundsException ignored) { + return context.getString(R.string.file); + } + if (mime == null) { + return context.getString(R.string.file); + } else if (mime.startsWith("audio/")) { + return context.getString(R.string.audio); + } else if(mime.startsWith("video/")) { + return context.getString(R.string.video); + } else if (mime.startsWith("image/")) { + return context.getString(R.string.image); + } else if (mime.contains("pdf")) { + return context.getString(R.string.pdf_document) ; + } else if (mime.contains("application/vnd.android.package-archive")) { + return context.getString(R.string.apk) ; + } else if (mime.contains("vcard")) { + return context.getString(R.string.vcard) ; + } else { + return mime; + } + } + + public static String getMessageDisplayName(final Message message) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + return getDisplayedMucCounterpart(message.getCounterpart()); + } else { + final Contact contact = message.getContact(); + return contact != null ? contact.getDisplayName() : ""; + } + } else { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + return getDisplayedMucCounterpart(message.getConversation().getJid()); + } else { + final Jid jid = message.getConversation().getAccount().getJid(); + return jid.hasLocalpart() ? jid.getLocalpart() : jid.toDomainJid().toString(); + } + } + } + + private static String getDisplayedMucCounterpart(final Jid counterpart) { + if (counterpart==null) { + return ""; + } else if (!counterpart.isBareJid()) { + return counterpart.getResourcepart().trim(); + } else { + return counterpart.toString().trim(); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/XmlHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/XmlHelper.java new file mode 100644 index 00000000..1287f73f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/XmlHelper.java @@ -0,0 +1,12 @@ +package de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/utils/Xmlns.java b/src/main/java/de/thedevstack/conversationsplus/utils/Xmlns.java new file mode 100644 index 00000000..80718dec --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/Xmlns.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.utils; + +public final class Xmlns { + public static final String BLOCKING = "urn:xmpp:blocking"; + public static final String ROSTER = "jabber:iq:roster"; + public static final String REGISTER = "jabber:iq:register"; + public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/XmppUri.java b/src/main/java/de/thedevstack/conversationsplus/utils/XmppUri.java new file mode 100644 index 00000000..5d3a2694 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/XmppUri.java @@ -0,0 +1,85 @@ +package de.thedevstack.conversationsplus.utils; + +import android.net.Uri; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class XmppUri { + + protected String jid; + protected boolean muc; + protected String fingerprint; + + public XmppUri(String uri) { + try { + parse(Uri.parse(uri)); + } catch (IllegalArgumentException e) { + try { + jid = Jid.fromString(uri).toBareJid().toString(); + } catch (InvalidJidException e2) { + jid = null; + } + } + } + + public XmppUri(Uri uri) { + parse(uri); + } + + protected void parse(Uri uri) { + String scheme = uri.getScheme(); + if ("xmpp".equalsIgnoreCase(scheme)) { + // sample: xmpp:jid@foo.com + muc = "join".equalsIgnoreCase(uri.getQuery()); + if (uri.getAuthority() != null) { + jid = uri.getAuthority(); + } else { + jid = uri.getSchemeSpecificPart().split("\\?")[0]; + } + fingerprint = parseFingerprint(uri.getQuery()); + } else if ("imto".equalsIgnoreCase(scheme)) { + // sample: imto://xmpp/jid@foo.com + try { + jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1]; + } catch (final UnsupportedEncodingException ignored) { + jid = null; + } + } else { + try { + jid = Jid.fromString(uri.toString()).toBareJid().toString(); + } catch (final InvalidJidException ignored) { + jid = null; + } + } + } + + protected String parseFingerprint(String query) { + if (query == null) { + return null; + } else { + final String NEEDLE = "otr-fingerprint="; + int index = query.indexOf(NEEDLE); + if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) { + return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40); + } else { + return null; + } + } + } + + public Jid getJid() { + try { + return this.jid == null ? null :Jid.fromString(this.jid.toLowerCase()); + } catch (InvalidJidException e) { + return null; + } + } + + public String getFingerprint() { + return this.fingerprint; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xml/Element.java b/src/main/java/de/thedevstack/conversationsplus/xml/Element.java new file mode 100644 index 00000000..d0cb87f9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xml/Element.java @@ -0,0 +1,171 @@ +package de.thedevstack.conversationsplus.xml; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.utils.XmlHelper; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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(final String name) { + return findChild(name) != null; + } + + public boolean hasChild(final String name, final 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 Jid getAttributeAsJid(String name) { + final String jid = this.getAttribute(name); + if (jid != null && !jid.isEmpty()) { + try { + return Jid.fromString(jid); + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "could not parse jid " + jid); + return null; + } + } + 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)); + } + + public boolean getAttributeAsBoolean(String name) { + String attr = getAttribute(name); + return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xml/Tag.java b/src/main/java/de/thedevstack/conversationsplus/xml/Tag.java new file mode 100644 index 00000000..84e37363 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xml/Tag.java @@ -0,0 +1,104 @@ +package de.thedevstack.conversationsplus.xml; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; + +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/xml/TagWriter.java b/src/main/java/de/thedevstack/conversationsplus/xml/TagWriter.java new file mode 100644 index 00000000..c0f028af --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xml/TagWriter.java @@ -0,0 +1,114 @@ +package de.thedevstack.conversationsplus.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.concurrent.LinkedBlockingQueue; + +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/xml/XmlReader.java b/src/main/java/de/thedevstack/conversationsplus/xml/XmlReader.java new file mode 100644 index 00000000..d7f6edf5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xml/XmlReader.java @@ -0,0 +1,141 @@ +package de.thedevstack.conversationsplus.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/xmpp/OnAdvancedStreamFeaturesLoaded.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnAdvancedStreamFeaturesLoaded.java new file mode 100644 index 00000000..0692232c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnAdvancedStreamFeaturesLoaded.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnAdvancedStreamFeaturesLoaded { + public void onAdvancedStreamFeaturesAvailable(final Account account); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnBindListener.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnBindListener.java new file mode 100644 index 00000000..274511e0 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnBindListener.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnBindListener { + public void onBind(Account account); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnContactStatusChanged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnContactStatusChanged.java new file mode 100644 index 00000000..009b33df --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnContactStatusChanged.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Contact; + +public interface OnContactStatusChanged { + public void onContactStatusChanged(final Contact contact, final boolean online); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnIqPacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnIqPacketReceived.java new file mode 100644 index 00000000..df0178c6 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnIqPacketReceived.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public interface OnIqPacketReceived extends PacketReceived { + public void onIqPacketReceived(Account account, IqPacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessageAcknowledged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessageAcknowledged.java new file mode 100644 index 00000000..7cfa557d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessageAcknowledged.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnMessageAcknowledged { + public void onMessageAcknowledged(Account account, String id); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessagePacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessagePacketReceived.java new file mode 100644 index 00000000..47649a18 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessagePacketReceived.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; + +public interface OnMessagePacketReceived extends PacketReceived { + public void onMessagePacketReceived(Account account, MessagePacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnPresencePacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnPresencePacketReceived.java new file mode 100644 index 00000000..70b0ccd2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnPresencePacketReceived.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket; + +public interface OnPresencePacketReceived extends PacketReceived { + public void onPresencePacketReceived(Account account, PresencePacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnStatusChanged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnStatusChanged.java new file mode 100644 index 00000000..c8478bbb --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnStatusChanged.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnStatusChanged { + public void onStatusChanged(Account account); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnUpdateBlocklist.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnUpdateBlocklist.java new file mode 100644 index 00000000..ea7b8c62 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnUpdateBlocklist.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.xmpp; + +public interface OnUpdateBlocklist { + // Use an enum instead of a boolean to make sure we don't run into the boolean trap + // (`onUpdateBlocklist(true)' doesn't read well, and could be confusing). + public static enum Status { + BLOCKED, + UNBLOCKED + } + + @SuppressWarnings("MethodNameSameAsClassName") + public void OnUpdateBlocklist(final Status status); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java new file mode 100644 index 00000000..6274aaa2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java @@ -0,0 +1,5 @@ +package de.thedevstack.conversationsplus.xmpp; + +public abstract interface PacketReceived { + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java new file mode 100644 index 00000000..039d8375 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java @@ -0,0 +1,1125 @@ +package de.thedevstack.conversationsplus.xmpp; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.ConnectException; +import java.net.IDN; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +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 de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.crypto.sasl.DigestMd5; +import de.thedevstack.conversationsplus.crypto.sasl.Plain; +import de.thedevstack.conversationsplus.crypto.sasl.SaslMechanism; +import de.thedevstack.conversationsplus.crypto.sasl.ScramSha1; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.generator.IqGenerator; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.DNSHelper; +import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xml.Tag; +import de.thedevstack.conversationsplus.xml.TagWriter; +import de.thedevstack.conversationsplus.xml.XmlReader; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.jingle.OnJinglePacketReceived; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.AbstractStanza; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.csi.ActivePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.csi.InactivePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.AckPacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.EnablePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.RequestPacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.ResumePacket; + +public class XmppConnection implements Runnable { + + private static final int PACKET_IQ = 0; + private static final int PACKET_MESSAGE = 1; + private static final int PACKET_PRESENCE = 2; + private final Context applicationContext; + protected Account account; + private final WakeLock wakeLock; + private Socket socket; + private XmlReader tagReader; + private TagWriter tagWriter; + private final Features features = new Features(this); + private boolean shouldBind = true; + private boolean shouldAuthenticate = true; + private Element streamFeatures; + private final HashMap> disco = new HashMap<>(); + + private String streamId = null; + private int smVersion = 3; + private final SparseArray messageReceipts = new SparseArray<>(); + + private int stanzasReceived = 0; + private int stanzasSent = 0; + private long lastPacketReceived = 0; + private long lastPingSent = 0; + private long lastConnect = 0; + private long lastSessionStarted = 0; + private int attempt = 0; + private final Map> 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 final ArrayList advancedStreamFeaturesLoadedListeners = new ArrayList<>(); + private OnMessageAcknowledged acknowledgedListener = null; + private XmppConnectionService mXmppConnectionService = null; + + private SaslMechanism saslMechanism; + + public XmppConnection(final Account account, final XmppConnectionService service) { + this.account = account; + this.wakeLock = service.getPowerManager().newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); + tagWriter = new TagWriter(); + mXmppConnectionService = service; + applicationContext = service.getApplicationContext(); + } + + protected void changeStatus(final Account.State nextStatus) { + if (account.getStatus() != nextStatus) { + if ((nextStatus == Account.State.OFFLINE) + && (account.getStatus() != Account.State.CONNECTING) + && (account.getStatus() != Account.State.ONLINE) + && (account.getStatus() != Account.State.DISABLED)) { + return; + } + if (nextStatus == Account.State.ONLINE) { + this.attempt = 0; + } + account.setStatus(nextStatus); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + protected void connect() { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); + features.encryptionEnabled = 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.State.CONNECTING); + final Bundle result = DNSHelper.getSRVRecord(account.getServer()); + final ArrayList values = result.getParcelableArrayList("values"); + if ("timeout".equals(result.getString("error"))) { + throw new IOException("timeout in dns"); + } else if (values != null) { + int i = 0; + boolean socketError = true; + while (socketError && values.size() > i) { + final Bundle namePort = (Bundle) values.get(i); + try { + String srvRecordServer; + try { + srvRecordServer=IDN.toASCII(namePort.getString("name")); + } catch (final IllegalArgumentException e) { + // TODO: Handle me?` + srvRecordServer = ""; + } + final int srvRecordPort = namePort.getInt("port"); + final String srvIpServer = namePort.getString("ip"); + final InetSocketAddress addr; + if (srvIpServer != null) { + addr = new InetSocketAddress(srvIpServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": using values from dns " + srvRecordServer + + "[" + srvIpServer + "]:" + srvRecordPort); + } else { + addr = new InetSocketAddress(srvRecordServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + } + socket = new Socket(); + socket.connect(addr, 20000); + socketError = false; + } catch (final UnknownHostException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + i++; + } catch (final IOException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + i++; + } + } + if (socketError) { + throw new UnknownHostException(); + } + } else if (result.containsKey("error") + && "nosrv".equals(result.getString("error", null))) { + socket = new Socket(account.getServer().getDomainpart(), 5222); + } else { + throw new IOException("timeout in dns"); + } + final OutputStream out = socket.getOutputStream(); + tagWriter.setOutputStream(out); + final 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 { + throw new IOException("unknown tag on connect"); + } + } + if (socket.isConnected()) { + socket.close(); + } + } catch (final UnknownHostException | ConnectException e) { + this.changeStatus(Account.State.SERVER_NOT_FOUND); + } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + this.changeStatus(Account.State.OFFLINE); + this.attempt--; //don't count attempt when reconnecting instantly anyway + } finally { + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (final RuntimeException ignored) { + } + } + } + } + + @Override + public void run() { + try { + if (socket != null) { + socket.close(); + } + } catch (final IOException ignored) { + + } + connect(); + } + + private void processStream(final 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("success")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + try { + saslMechanism.getResponse(challenge); + } catch (final SaslMechanism.AuthenticationException e) { + disconnect(true); + Log.e(Config.LOGTAG, String.valueOf(e)); + } + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in"); + account.setKey(Account.PINNED_MECHANISM_KEY, + String.valueOf(saslMechanism.getPriority())); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + tagReader.readElement(nextTag); + changeStatus(Account.State.UNAUTHORIZED); + } else if (nextTag.isStart("challenge")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + final Element response = new Element("response"); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + try { + response.setContent(saslMechanism.getResponse(challenge)); + } catch (final SaslMechanism.AuthenticationException e) { + // TODO: Send auth abort tag. + Log.e(Config.LOGTAG, e.toString()); + } + tagWriter.writeElement(response); + } else if (nextTag.isStart("enabled")) { + final Element enabled = tagReader.readElement(nextTag); + if ("true".equals(enabled.getAttribute("resume"))) { + this.streamId = enabled.getAttribute("id"); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": stream managment(" + smVersion + + ") enabled (resumable)"); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": stream managment(" + smVersion + ") enabled"); + } + this.lastSessionStarted = SystemClock.elapsedRealtime(); + this.stanzasReceived = 0; + final RequestPacket r = new RequestPacket(smVersion); + tagWriter.writeStanzaAsync(r); + } else if (nextTag.isStart("resumed")) { + lastPacketReceived = SystemClock.elapsedRealtime(); + final Element resumed = tagReader.readElement(nextTag); + final String h = resumed.getAttribute("h"); + try { + final int serverCount = Integer.parseInt(h); + if (serverCount != stanzasSent) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": 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 (final NumberFormatException ignored) { + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + sendInitialPing(); + } else if (nextTag.isStart("r")) { + tagReader.readElement(nextTag); + final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + tagWriter.writeStanzaAsync(ack); + } else if (nextTag.isStart("a")) { + final Element ack = tagReader.readElement(nextTag); + lastPacketReceived = SystemClock.elapsedRealtime(); + final int serverSequence = Integer.parseInt(ack.getAttribute("h")); + final 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().toBareJid().toString() + ": resumption failed"); + streamId = null; + if (account.getStatus() != Account.State.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.State.ONLINE) { + account. setStatus(Account.State.OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + private void sendInitialPing() { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping"); + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setFrom(account.getJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": online with resource " + account.getResource()); + changeStatus(Account.State.ONLINE); + } + }); + } + + private Element processPacket(final Tag currentTag, final 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()) { + final Element child = tagReader.readElement(nextTag); + final 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; + lastPacketReceived = SystemClock.elapsedRealtime(); + return element; + } + + private void processIq(final Tag currentTag) throws XmlPullParserException, IOException { + final 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())) { + final Pair packetCallbackDuple = packetCallbacks.get(packet.getId()); + // Packets to the server should have responses from the server + if (packetCallbackDuple.first.toServer(account)) { + if (packet.fromServer(account)) { + packetCallbackDuple.second.onIqPacketReceived(account, packet); + packetCallbacks.remove(packet.getId()); + } else { + Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet"); + } + } else { + if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) { + packetCallbackDuple.second.onIqPacketReceived(account, packet); + packetCallbacks.remove(packet.getId()); + } else { + Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet"); + } + } + } else if (packet.getType() == IqPacket.TYPE.GET|| packet.getType() == IqPacket.TYPE.SET) { + this.unregisteredIqListener.onIqPacketReceived(account, packet); + } + } + } + + private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException { + final MessagePacket packet = (MessagePacket) processPacket(currentTag,PACKET_MESSAGE); + this.messageListener.onMessagePacketReceived(account, packet); + } + + private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException { + PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); + this.presenceListener.onPresencePacketReceived(account, packet); + } + + private void sendStartTLS() throws IOException { + final 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(final Tag currentTag) throws XmlPullParserException, IOException { + tagReader.readTag(); + try { + final SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null,new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},mXmppConnectionService.getRNG()); + final SSLSocketFactory factory = sc.getSocketFactory(); + final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); + final InetAddress address = socket == null ? null : socket.getInetAddress(); + + if (factory == null || address == null || verifier == null) { + throw new IOException("could not setup ssl"); + } + + final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); + + if (sslSocket == null) { + throw new IOException("could not initialize ssl socket"); + } + + final String[] supportProtocols; + final Collection supportedProtocols = new LinkedList<>( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]); + + sslSocket.setEnabledProtocols(supportProtocols); + + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( + sslSocket.getSupportedCipherSuites()); + //Log.d(Config.LOGTAG, "Using ciphers: " + Arrays.toString(cipherSuites)); + if (cipherSuites.length > 0) { + sslSocket.setEnabledCipherSuites(cipherSuites); + } + + if (!verifier.verify(account.getServer().getDomainpart(),sslSocket.getSession())) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established"); + features.encryptionEnabled = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (final NoSuchAlgorithmException | KeyManagementException e1) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + } + + private void processStreamFeatures(final Tag currentTag) + throws XmlPullParserException, IOException { + this.streamFeatures = tagReader.readElement(currentTag); + if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) { + sendStartTLS(); + } else if (this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER) + && features.encryptionEnabled) { + sendRegistryRequest(); + } else if (!this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER)) { + changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED); + disconnect(true); + } else if (this.streamFeatures.hasChild("mechanisms") + && shouldAuthenticate && features.encryptionEnabled) { + final List mechanisms = extractMechanisms(streamFeatures + .findChild("mechanisms")); + final Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + if (mechanisms.contains("SCRAM-SHA-1")) { + saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); + } else if (mechanisms.contains("PLAIN")) { + saslMechanism = new Plain(tagWriter, account); + } else if (mechanisms.contains("DIGEST-MD5")) { + saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); + } + final JSONObject keys = account.getKeys(); + try { + if (keys.has(Account.PINNED_MECHANISM_KEY) && + keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority() ) { + Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + + " has lower priority (" + String.valueOf(saslMechanism.getPriority()) + + ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) + + "). Possible downgrade attack?"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + } catch (final JSONException e) { + Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); + } + Log.d(Config.LOGTAG,account.getJid().toString()+": Authenticating with " + saslMechanism.getMechanism()); + auth.setAttribute("mechanism", saslMechanism.getMechanism()); + if (!saslMechanism.getClientFirstMessage().isEmpty()) { + auth.setContent(saslMechanism.getClientFirstMessage()); + } + tagWriter.writeElement(auth); + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + + smVersion) + && streamId != null) { + final ResumePacket resume = new ResumePacket(this.streamId, + stanzasReceived, smVersion); + this.tagWriter.writeStanzaAsync(resume); + } else if (this.streamFeatures.hasChild("bind") && shouldBind) { + sendBindRequest(); + } else { + disconnect(true); + changeStatus(Account.State.INCOMPATIBLE_SERVER); + } + } + + private List extractMechanisms(final Element stream) { + final ArrayList mechanisms = new ArrayList<>(stream + .getChildren().size()); + for (final Element child : stream.getChildren()) { + mechanisms.add(child.getContent()); + } + return mechanisms; + } + + private void sendRegistryRequest() { + final IqPacket register = new IqPacket(IqPacket.TYPE.GET); + register.query("jabber:iq:register"); + register.setTo(account.getServer()); + sendIqPacket(register, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final Element instructions = packet.query().findChild("instructions"); + if (packet.query().hasChild("username") + && (packet.query().hasChild("password"))) { + final IqPacket register = new IqPacket(IqPacket.TYPE.SET); + final Element username = new Element("username") + .setContent(account.getUsername()); + final 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(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.setOption(Account.OPTION_REGISTER, + false); + changeStatus(Account.State.REGISTRATION_SUCCESSFUL); + } else if (packet.hasChild("error") + && (packet.findChild("error") + .hasChild("conflict"))) { + changeStatus(Account.State.REGISTRATION_CONFLICT); + } else { + changeStatus(Account.State.REGISTRATION_FAILED); + Log.d(Config.LOGTAG, packet.toString()); + } + disconnect(true); + } + }); + } else { + changeStatus(Account.State.REGISTRATION_FAILED); + disconnect(true); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": could not register. instructions are" + + instructions.getContent()); + } + } + }); + } + + private void sendBindRequest() { + while(!mXmppConnectionService.areMessagesInitialized()) { + try { + Thread.sleep(500); + } catch (final InterruptedException ignored) { + } + } + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") + .addChild("resource").setContent(account.getResource()); + this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final Element bind = packet.findChild("bind"); + if (bind != null) { + final Element jid = bind.findChild("jid"); + if (jid != null && jid.getContent() != null) { + try { + account.setResource(Jid.fromString(jid.getContent()).getResourcepart()); + } catch (final InvalidJidException e) { + // TODO: Handle the case where an external JID is technically invalid? + } + if (streamFeatures.hasChild("session")) { + sendStartSession(); + } else { + sendPostBindInitialization(); + } + } else { + disconnect(true); + } + } else { + disconnect(true); + } + } + }); + } + + private void sendStartSession() { + final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); + startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); + this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + sendPostBindInitialization(); + } else { + disconnect(true); + } + } + }); + } + + private void sendPostBindInitialization() { + smVersion = 0; + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { + smVersion = 3; + } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { + smVersion = 2; + } + if (smVersion != 0) { + final EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); + } + features.carbonsEnabled = false; + features.blockListRequested = false; + disco.clear(); + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + if (bindListener != null) { + bindListener.onBind(account); + } + sendInitialPing(); + } + + private void sendServiceDiscoveryInfo(final Jid server) { + if (disco.containsKey(server.toDomainJid().toString())) { + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + } + } else { + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setTo(server.toDomainJid()); + iq.query("http://jabber.org/protocol/disco#info"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final List elements = packet.query().getChildren(); + final List features = new ArrayList<>(); + for (final Element element : elements) { + if (element.getName().equals("identity")) { + if ("irc".equals(element.getAttribute("type"))) { + //add fake feature to not confuse irc and real muc + features.add("siacs:no:muc"); + } + } else if (element.getName().equals("feature")) { + features.add(element.getAttribute("var")); + } + } + disco.put(server.toDomainJid().toString(), features); + + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { + listener.onAdvancedStreamFeaturesAvailable(account); + } + } + } + }); + } + } + + private void enableAdvancedStreamFeatures() { + if (getFeatures().carbons() && !features.carbonsEnabled) { + sendEnableCarbons(); + } + if (getFeatures().blocking() && !features.blockListRequested) { + Log.d(Config.LOGTAG, "Requesting block list"); + this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); + } + } + + private void sendServiceDiscoveryItems(final Jid server) { + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setTo(server.toDomainJid()); + iq.query("http://jabber.org/protocol/disco#items"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final List elements = packet.query().getChildren(); + for (final Element element : elements) { + if (element.getName().equals("item")) { + final Jid jid = element.getAttributeAsJid("jid"); + if (jid != null && !jid.equals(account.getServer())) { + sendServiceDiscoveryInfo(jid); + } + } + } + } + }); + } + + private void sendEnableCarbons() { + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.addChild("enable", "urn:xmpp:carbons:2"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (!packet.hasChild("error")) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": successfully enabled carbons"); + features.carbonsEnabled = true; + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": error enableing carbons " + packet.toString()); + } + } + }); + } + + private void processStreamError(final Tag currentTag) + throws XmlPullParserException, IOException { + final Element streamError = tagReader.readElement(currentTag); + if (streamError != null && streamError.hasChild("conflict")) { + final String resource = account.getResource().split("\\.")[0]; + account.setResource(resource + "." + nextRandomId()); + Log.d(Config.LOGTAG, + account.getJid().toBareJid() + ": switching resource due to conflict (" + + account.getResource() + ")"); + } + } + + private void sendStartStream() throws IOException { + final Tag stream = Tag.start("stream:stream"); + stream.setAttribute("from", account.getJid().toBareJid().toString()); + stream.setAttribute("to", account.getServer().toString()); + 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, mXmppConnectionService.getRNG()).toString(32); + } + + public void sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { + packet.setFrom(account.getJid()); + this.sendUnmodifiedIqPacket(packet,callback); + + } + + private synchronized void sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { + if (packet.getId() == null) { + final String id = nextRandomId(); + packet.setAttribute("id", id); + } + if (callback != null) { + if (packet.getId() == null) { + packet.setId(nextRandomId()); + } + packetCallbacks.put(packet.getId(), new Pair<>(packet, callback)); + } + this.sendPacket(packet); + } + + public void sendMessagePacket(final MessagePacket packet) { + this.sendPacket(packet); + } + + public void sendPresencePacket(final PresencePacket packet) { + this.sendPacket(packet); + } + + private synchronized void sendPacket(final AbstractStanza packet) { + final String name = packet.getName(); + if (name.equals("iq") || name.equals("message") || name.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)); + } + } + + public void sendPing() { + if (streamFeatures.hasChild("sm")) { + tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } else { + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setFrom(account.getJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, null); + } + this.lastPingSent = SystemClock.elapsedRealtime(); + } + + public void setOnMessagePacketReceivedListener( + final OnMessagePacketReceived listener) { + this.messageListener = listener; + } + + public void setOnUnregisteredIqPacketReceivedListener( + final OnIqPacketReceived listener) { + this.unregisteredIqListener = listener; + } + + public void setOnPresencePacketReceivedListener( + final OnPresencePacketReceived listener) { + this.presenceListener = listener; + } + + public void setOnJinglePacketReceivedListener( + final OnJinglePacketReceived listener) { + this.jingleListener = listener; + } + + public void setOnStatusChangedListener(final OnStatusChanged listener) { + this.statusListener = listener; + } + + public void setOnBindListener(final OnBindListener listener) { + this.bindListener = listener; + } + + public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) { + this.acknowledgedListener = listener; + } + + public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) { + if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) { + this.advancedStreamFeaturesLoadedListeners.add(listener); + } + } + + public void disconnect(final boolean force) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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 (final IOException e) { + Log.d(Config.LOGTAG, + "io exception during disconnect"); + } catch (final InterruptedException e) { + Log.d(Config.LOGTAG, "interrupted"); + } + } + } + }).start(); + } catch (final IOException e) { + Log.d(Config.LOGTAG, "io exception during disconnect"); + } + } + + public List findDiscoItemsByFeature(final String feature) { + final List items = new ArrayList<>(); + for (final Entry> cursor : disco.entrySet()) { + if (cursor.getValue().contains(feature)) { + items.add(cursor.getKey()); + } + } + return items; + } + + public String findDiscoItemByFeature(final String feature) { + final 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() { + for (final Entry> cursor : disco.entrySet()) { + final List value = cursor.getValue(); + if (value.contains("http://jabber.org/protocol/muc") && !value.contains("jabber:iq:gateway") && !value.contains("siacs:no:muc")) { + return cursor.getKey(); + } + } + return null; + } + + public int getTimeToNextAttempt() { + final int interval = (int) (25 * Math.pow(1.5, attempt)); + final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + return interval - secondsSinceLast; + } + + public int getAttempt() { + return this.attempt; + } + + public Features getFeatures() { + return this.features; + } + + public long getLastSessionEstablished() { + final 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.lastPacketReceived; + } + + public void sendActive() { + this.sendPacket(new ActivePacket()); + } + + public void sendInactive() { + this.sendPacket(new InactivePacket()); + } + + public void resetAttemptCount() { + this.attempt = 0; + this.lastConnect = 0; + } + + public class Features { + XmppConnection connection; + private boolean carbonsEnabled = false; + private boolean encryptionEnabled = false; + private boolean blockListRequested = false; + + public Features(final XmppConnection connection) { + this.connection = connection; + } + + private boolean hasDiscoFeature(final Jid server, final String feature) { + return connection.disco.containsKey(server.toDomainJid().toString()) && + connection.disco.get(server.toDomainJid().toString()).contains(feature); + } + + public boolean carbons() { + return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); + } + + public boolean blocking() { + return hasDiscoFeature(account.getServer(), Xmlns.BLOCKING); + } + + public boolean register() { + return hasDiscoFeature(account.getServer(), Xmlns.REGISTER); + } + + public boolean sm() { + return streamId != null; + } + + public boolean csi() { + return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0"); + } + + public boolean pubsub() { + return hasDiscoFeature(account.getServer(), + "http://jabber.org/protocol/pubsub#publish"); + } + + public boolean mam() { + return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); + } + + public boolean advancedStreamFeaturesLoaded() { + return disco.containsKey(account.getServer().toString()); + } + + public boolean rosterVersioning() { + return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver"); + } + + public void setBlockListRequested(boolean value) { + this.blockListRequested = value; + } + } + + private IqGenerator getIqGenerator() { + return mXmppConnectionService.getIqGenerator(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/chatstate/ChatState.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatstate/ChatState.java new file mode 100644 index 00000000..929d485a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatstate/ChatState.java @@ -0,0 +1,32 @@ +package de.thedevstack.conversationsplus.xmpp.chatstate; + +import de.thedevstack.conversationsplus.xml.Element; + +public enum ChatState { + + ACTIVE, INACTIVE, GONE, COMPOSING, PAUSED, mIncomingChatState; + + public static ChatState parse(Element element) { + final String NAMESPACE = "http://jabber.org/protocol/chatstates"; + if (element.hasChild("active",NAMESPACE)) { + return ACTIVE; + } else if (element.hasChild("inactive",NAMESPACE)) { + return INACTIVE; + } else if (element.hasChild("composing",NAMESPACE)) { + return COMPOSING; + } else if (element.hasChild("gone",NAMESPACE)) { + return GONE; + } else if (element.hasChild("paused",NAMESPACE)) { + return PAUSED; + } else { + return null; + } + } + + public static Element toElement(ChatState state) { + final String NAMESPACE = "http://jabber.org/protocol/chatstates"; + final Element element = new Element(state.toString().toLowerCase()); + element.setAttribute("xmlns",NAMESPACE); + return element; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Data.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Data.java new file mode 100644 index 00000000..591d8ada --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Data.java @@ -0,0 +1,85 @@ +package de.thedevstack.conversationsplus.xmpp.forms; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Data extends Element { + + public Data() { + super("x"); + this.setAttribute("xmlns","jabber:x:data"); + } + + public List getFields() { + ArrayList fields = new ArrayList(); + for(Element child : getChildren()) { + if (child.getName().equals("field")) { + fields.add(Field.parse(child)); + } + } + return fields; + } + + public Field getFieldByName(String needle) { + for(Element child : getChildren()) { + if (child.getName().equals("field") && needle.equals(child.getAttribute("var"))) { + return Field.parse(child); + } + } + return null; + } + + public void put(String name, String value) { + Field field = getFieldByName(name); + if (field == null) { + field = new Field(name); + this.addChild(field); + } + field.setValue(value); + } + + public void put(String name, Collection values) { + Field field = getFieldByName(name); + if (field == null) { + field = new Field(name); + this.addChild(field); + } + field.setValues(values); + } + + public void submit() { + this.setAttribute("type","submit"); + removeNonFieldChildren(); + for(Field field : getFields()) { + field.removeNonValueChildren(); + } + } + + private void removeNonFieldChildren() { + for(Iterator iterator = this.children.iterator(); iterator.hasNext();) { + Element element = iterator.next(); + if (!element.getName().equals("field")) { + iterator.remove(); + } + } + } + + public static Data parse(Element element) { + Data data = new Data(); + data.setAttributes(element.getAttributes()); + data.setChildren(element.getChildren()); + return data; + } + + public void setFormType(String formType) { + this.put("FORM_TYPE",formType); + } + + public String getFormType() { + return this.getAttribute("FORM_TYPE"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Field.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Field.java new file mode 100644 index 00000000..9eb2d08d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Field.java @@ -0,0 +1,50 @@ +package de.thedevstack.conversationsplus.xmpp.forms; + +import java.util.Collection; +import java.util.Iterator; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Field extends Element { + + public Field(String name) { + super("field"); + this.setAttribute("var",name); + } + + private Field() { + super("field"); + } + + public String getName() { + return this.getAttribute("var"); + } + + public void setValue(String value) { + this.children.clear(); + this.addChild("value").setContent(value); + } + + public void setValues(Collection values) { + this.children.clear(); + for(String value : values) { + this.addChild("value").setContent(value); + } + } + + public void removeNonValueChildren() { + for(Iterator iterator = this.children.iterator(); iterator.hasNext();) { + Element element = iterator.next(); + if (!element.getName().equals("value")) { + iterator.remove(); + } + } + } + + public static Field parse(Element element) { + Field field = new Field(); + field.setAttributes(element.getAttributes()); + field.setChildren(element.getChildren()); + return field; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/InvalidJidException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/InvalidJidException.java new file mode 100644 index 00000000..3c8dd717 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/InvalidJidException.java @@ -0,0 +1,49 @@ +package de.thedevstack.conversationsplus.xmpp.jid; + +public class InvalidJidException extends Exception { + + // This is probably not the "Java way", but the "Java way" means we'd have a ton of extra tiny, + // annoying classes floating around. I like this. + public final static String INVALID_LENGTH = "JID must be between 0 and 3071 characters"; + public final static String INVALID_PART_LENGTH = "JID part must be between 0 and 1023 characters"; + public final static String INVALID_CHARACTER = "JID contains an invalid character"; + public final static String STRINGPREP_FAIL = "The STRINGPREP operation has failed for the given JID"; + public final static String IS_NULL = "JID can not be NULL"; + + /** + * Constructs a new {@code Exception} that includes the current stack trace. + */ + public InvalidJidException() { + } + + /** + * Constructs a new {@code Exception} with the current stack trace and the + * specified detail message. + * + * @param detailMessage the detail message for this exception. + */ + public InvalidJidException(final String detailMessage) { + super(detailMessage); + } + + /** + * Constructs a new {@code Exception} with the current stack trace, the + * specified detail message and the specified cause. + * + * @param detailMessage the detail message for this exception. + * @param throwable the cause of this exception. + */ + public InvalidJidException(final String detailMessage, final Throwable throwable) { + super(detailMessage, throwable); + } + + /** + * Constructs a new {@code Exception} with the current stack trace and the + * specified cause. + * + * @param throwable the cause of this exception. + */ + public InvalidJidException(final Throwable throwable) { + super(throwable); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/Jid.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/Jid.java new file mode 100644 index 00000000..43b0e878 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/Jid.java @@ -0,0 +1,219 @@ +package de.thedevstack.conversationsplus.xmpp.jid; + +import android.util.LruCache; + +import net.java.otr4j.session.SessionID; + +import java.net.IDN; + +import de.thedevstack.conversationsplus.Config; +import gnu.inet.encoding.Stringprep; +import gnu.inet.encoding.StringprepException; + +/** + * The `Jid' class provides an immutable representation of a JID. + */ +public final class Jid { + + private static LruCache cache = new LruCache<>(1024); + + private final String localpart; + private final String domainpart; + private final String resourcepart; + + // It's much more efficient to store the ful JID as well as the parts instead of figuring them + // all out every time (since some characters are displayed but aren't used for comparisons). + private final String displayjid; + + public String getLocalpart() { + return localpart; + } + + public String getDomainpart() { + return IDN.toUnicode(domainpart); + } + + public String getResourcepart() { + return resourcepart; + } + + public static Jid fromSessionID(final SessionID id) throws InvalidJidException{ + if (id.getUserID().isEmpty()) { + return Jid.fromString(id.getAccountID()); + } else { + return Jid.fromString(id.getAccountID()+"/"+id.getUserID()); + } + } + + public static Jid fromString(final String jid) throws InvalidJidException { + return Jid.fromString(jid, false); + } + + public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException { + return new Jid(jid, safe); + } + + public static Jid fromParts(final String localpart, + final String domainpart, + final String resourcepart) throws InvalidJidException { + String out; + if (localpart == null || localpart.isEmpty()) { + out = domainpart; + } else { + out = localpart + "@" + domainpart; + } + if (resourcepart != null && !resourcepart.isEmpty()) { + out = out + "/" + resourcepart; + } + return new Jid(out, false); + } + + private Jid(final String jid, final boolean safe) throws InvalidJidException { + if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL); + + Jid fromCache = Jid.cache.get(jid); + if (fromCache != null) { + displayjid = fromCache.displayjid; + localpart = fromCache.localpart; + domainpart = fromCache.domainpart; + resourcepart = fromCache.resourcepart; + return; + } + + // Hackish Android way to count the number of chars in a string... should work everywhere. + final int atCount = jid.length() - jid.replace("@", "").length(); + final int slashCount = jid.length() - jid.replace("/", "").length(); + + // Throw an error if there's anything obvious wrong with the JID... + if (jid.isEmpty() || jid.length() > 3071) { + throw new InvalidJidException(InvalidJidException.INVALID_LENGTH); + } + + // Go ahead and check if the localpart or resourcepart is empty. + if (jid.startsWith("@") || (jid.endsWith("@") && slashCount == 0) || jid.startsWith("/") || (jid.endsWith("/") && slashCount < 2)) { + throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER); + } + + String finaljid; + + final int domainpartStart; + final int atLoc = jid.indexOf("@"); + final int slashLoc = jid.indexOf("/"); + // If there is no "@" in the JID (eg. "example.net" or "example.net/resource") + // or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"): + if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) { + localpart = ""; + finaljid = ""; + domainpartStart = 0; + } else { + final String lp = jid.substring(0, atLoc); + try { + localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp); + } catch (final StringprepException e) { + throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); + } + if (localpart.isEmpty() || localpart.length() > 1023) { + throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); + } + domainpartStart = atLoc + 1; + finaljid = lp + "@"; + } + + final String dp; + if (slashCount > 0) { + final String rp = jid.substring(slashLoc + 1, jid.length()); + try { + resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp); + } catch (final StringprepException e) { + throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); + } + if (resourcepart.isEmpty() || resourcepart.length() > 1023) { + throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); + } + dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES); + finaljid = finaljid + dp + "/" + rp; + } else { + resourcepart = ""; + dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()), + IDN.USE_STD3_ASCII_RULES); + finaljid = finaljid + dp; + } + + // Remove trailing "." before storing the domain part. + if (dp.endsWith(".")) { + try { + domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES); + } catch (final IllegalArgumentException e) { + throw new InvalidJidException(e); + } + } else { + try { + domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES); + } catch (final IllegalArgumentException e) { + throw new InvalidJidException(e); + } + } + + // TODO: Find a proper domain validation library; validate individual parts, separators, etc. + if (domainpart.isEmpty() || domainpart.length() > 1023) { + throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); + } + + Jid.cache.put(jid, this); + + this.displayjid = finaljid; + } + + public Jid toBareJid() { + try { + return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, ""); + } catch (final InvalidJidException e) { + // This should never happen. + return null; + } + } + + public Jid toDomainJid() { + try { + return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart()); + } catch (final InvalidJidException e) { + // This should never happen. + return null; + } + } + + @Override + public String toString() { + return displayjid; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Jid jid = (Jid) o; + + return jid.hashCode() == this.hashCode(); + } + + @Override + public int hashCode() { + int result = localpart.hashCode(); + result = 31 * result + domainpart.hashCode(); + result = 31 * result + resourcepart.hashCode(); + return result; + } + + public boolean hasLocalpart() { + return !localpart.isEmpty(); + } + + public boolean isBareJid() { + return this.resourcepart.isEmpty(); + } + + public boolean isDomainJid() { + return !this.hasLocalpart(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleCandidate.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleCandidate.java new file mode 100644 index 00000000..bf282293 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleCandidate.java @@ -0,0 +1,147 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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 Jid 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(final Jid jid) { + this.jid = jid; + } + + public Jid 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) { + switch (type) { + case "proxy": + this.type = TYPE_PROXY; + break; + case "direct": + this.type = TYPE_DIRECT; + break; + default: + this.type = TYPE_UNKNOWN; + break; + } + } + + 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 != null && 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.getAttributeAsJid("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().toString()); + 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/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java new file mode 100644 index 00000000..6348ec7e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java @@ -0,0 +1,971 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import java.net.URLConnection; +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.net.Uri; +import android.os.SystemClock; +import android.util.Log; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.DownloadablePlaceholder; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.Content; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.Reason; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class JingleConnection implements Downloadable { + + 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_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 = Downloadable.STATUS_UNKNOWN; + private Message message; + private String sessionId; + private Account account; + private Jid initiator; + private Jid 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 int mProgress = 0; + private long mLastGuiRefresh = 0; + + 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) { + fail(); + } + } + }; + + final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { + + @Override + public void onFileTransmitted(DownloadableFile file) { + if (responder.equals(account.getJid())) { + sendSuccess(); + if (acceptedAutomatically) { + message.markUnread(); + JingleConnection.this.mXmppConnectionService + .getNotificationService().push(message); + } + mXmppConnectionService.getFileBackend().updateFileParams(message); + mXmppConnectionService.databaseBackend.createMessage(message); + mXmppConnectionService.markMessage(message, + Message.STATUS_RECEIVED); + } else { + if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + file.delete(); + } + } + 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.fail(); + } + }; + + private OnProxyActivated onProxyActivated = new OnProxyActivated() { + + @Override + public void success() { + if (initiator.equals(account.getJid())) { + 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 Jid 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.fail(); + } else if (reason.hasChild("success")) { + this.receiveSuccess(); + } else { + this.fail(); + } + } else { + this.fail(); + } + } 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.generateResponse(IqPacket.TYPE.RESULT); + + } else { + response = packet.generateResponse(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.message.setDownloadable(this); + this.mStatus = Downloadable.STATUS_UPLOADING; + this.account = message.getConversation().getAccount(); + this.initiator = this.account.getJid(); + 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().toBareJid(), false); + this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); + this.message.setStatus(Message.STATUS_RECEIVED); + this.mStatus = Downloadable.STATUS_OFFER; + this.message.setDownloadable(this); + final Jid from = packet.getFrom(); + this.message.setCounterpart(from); + this.account = account; + this.initiator = packet.getFrom(); + this.responder = this.account.getJid(); + 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) { + String[] filename = fileNameElement.getContent() + .toLowerCase(Locale.US).toLowerCase().split("\\."); + String extension = filename[filename.length - 1]; + if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { + message.setType(Message.TYPE_IMAGE); + message.setRelativeFilePath(message.getUuid()+"."+extension); + } else if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains( + filename[filename.length - 1])) { + if (filename.length == 3) { + extension = filename[filename.length - 2]; + if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { + message.setType(Message.TYPE_IMAGE); + message.setRelativeFilePath(message.getUuid()+"."+extension); + } else { + message.setType(Message.TYPE_FILE); + } + if (filename[filename.length - 1].equals("otr")) { + message.setEncryption(Message.ENCRYPTION_OTR); + } else { + message.setEncryption(Message.ENCRYPTION_PGP); + } + } + } else { + message.setType(Message.TYPE_FILE); + } + if (message.getType() == Message.TYPE_FILE) { + String suffix = ""; + if (!fileNameElement.getContent().isEmpty()) { + String parts[] = fileNameElement.getContent().split("/"); + suffix = parts[parts.length - 1]; + if (message.getEncryption() == Message.ENCRYPTION_OTR && suffix.endsWith(".otr")) { + suffix = suffix.substring(0,suffix.length() - 4); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) { + suffix = suffix.substring(0,suffix.length() - 4); + } + } + message.setRelativeFilePath(message.getUuid()+"_"+suffix); + } + long size = Long.parseLong(fileSize.getContent()); + message.setBody(Long.toString(size)); + conversation.add(message); + mXmppConnectionService.updateConversationUi(); + if (size <= this.mJingleConnectionManager.getAutoAcceptFileSize() + && mXmppConnectionService.isDownloadAllowedInConnection()) { + 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.fail(); + return; + } else { + this.file.setKey(key); + } + } + this.file.setExpectedSize(size); + } else { + this.sendCancel(); + this.fail(); + } + } else { + this.sendCancel(); + this.fail(); + } + } + + private void sendInitRequest() { + JinglePacket packet = this.bootstrapPacket("session-initiate"); + Content content = new Content(this.contentCreator, this.contentName); + if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + 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,new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE.ERROR) { + mJingleStatus = JINGLE_STATUS_INITIATED; + mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED); + } else { + fail(); + } + } + }); + + } + } + + 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 && candidate != null && !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.getJid()); + packet.setTo(this.message.getCounterpart()); + packet.setSessionId(this.sessionId); + packet.setInitiator(this.initiator); + return packet; + } + + private void sendJinglePacket(JinglePacket packet) { + account.getXmppConnection().sendIqPacket(packet, responseListener); + } + + private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) { + account.getXmppConnection().sendIqPacket(packet,callback); + } + + 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.fail(); + } + } + 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.disconnectSocks5Connections(); + if (this.initiator.equals(account.getJid())) { + 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().toString()); + 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.getJid())) { + 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.getJid())) { + 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.disconnectSocks5Connections(); + 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, 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, 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_RECEIVED); + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } + this.message.setDownloadable(null); + this.mJingleConnectionManager.finishConnection(this); + } + + public void cancel() { + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } + this.sendCancel(); + this.mJingleConnectionManager.finishConnection(this); + if (this.responder.equals(account.getJid())) { + this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); + if (this.file!=null) { + file.delete(); + } + this.mXmppConnectionService.updateConversationUi(); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + this.message.setDownloadable(null); + } + } + + private void fail() { + this.mJingleStatus = JINGLE_STATUS_FAILED; + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } + if (this.message != null) { + if (this.responder.equals(account.getJid())) { + this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); + if (this.file!=null) { + file.delete(); + } + this.mXmppConnectionService.updateConversationUi(); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + this.message.setDownloadable(null); + } + } + 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 disconnectSocks5Connections() { + 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 Jid getInitiator() { + return this.initiator; + } + + public Jid 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; + } + + public void updateProgress(int i) { + this.mProgress = i; + if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { + this.mLastGuiRefresh = SystemClock.elapsedRealtime(); + mXmppConnectionService.updateConversationUi(); + } + } + + 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.State.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; + } + } + + @Override + public int getProgress() { + return this.mProgress; + } + + @Override + public String getMimeType() { + if (this.message.getType() == Message.TYPE_FILE) { + String mime = null; + String path = this.message.getRelativeFilePath(); + if (path != null && !this.message.getRelativeFilePath().isEmpty()) { + mime = URLConnection.guessContentTypeFromName(this.message.getRelativeFilePath()); + if (mime!=null) { + return mime; + } else { + return ""; + } + } else { + return ""; + } + } else { + return "image/webp"; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java new file mode 100644 index 00000000..eb1bfe2e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java @@ -0,0 +1,164 @@ +package de.thedevstack.conversationsplus.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 de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.AbstractConnectionManager; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; +import de.thedevstack.conversationsplus.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; + } + } + IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("item-not-found", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); + account.getXmppConnection().sendIqPacket(response, null); + } + } + + public JingleConnection createNewConnection(Message message) { + JingleConnection connection = new JingleConnection(this); + connection.init(message); + this.connections.add(connection); + return connection; + } + + public JingleConnection createNewConnection(final 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 (Config.NO_PROXY_LOOKUP) { + listener.onPrimaryCandidateFound(false, null); + return; + } + if (!this.primaryCandidates.containsKey(account.getJid().toBareJid())) { + final String proxy = account.getXmppConnection().findDiscoItemByFeature(Xmlns.BYTE_STREAMS); + if (proxy != null) { + IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setAttribute("to", proxy); + iq.query(Xmlns.BYTE_STREAMS); + account.getXmppConnection().sendIqPacket(iq,new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element streamhost = packet.query().findChild("streamhost",Xmlns.BYTE_STREAMS); + final String host = streamhost == null ? null : streamhost.getAttribute("host"); + final String port = streamhost == null ? null : streamhost.getAttribute("port"); + if (host != null && port != null) { + try { + JingleCandidate candidate = new JingleCandidate(nextRandomId(), true); + candidate.setHost(host); + candidate.setPort(Integer.parseInt(port)); + candidate.setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(Jid.fromString(proxy)); + candidate.setPriority(655360 + 65535); + primaryCandidates.put(account.getJid().toBareJid(),candidate); + listener.onPrimaryCandidateFound(true,candidate); + } catch (final NumberFormatException | InvalidJidException e) { + listener.onPrimaryCandidateFound(false,null); + return; + } + } else { + listener.onPrimaryCandidateFound(false,null); + } + } + }); + } else { + listener.onPrimaryCandidateFound(false, null); + } + + } else { + listener.onPrimaryCandidateFound(true, + this.primaryCandidates.get(account.getJid().toBareJid())); + } + } + + 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,"couldn't deliver payload: " + payload.toString()); + } else { + Log.d(Config.LOGTAG, "no sid found in incoming ibb packet"); + } + } + + public void cancelInTransmission() { + for (JingleConnection connection : this.connections) { + if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { + connection.cancel(); + } + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java new file mode 100644 index 00000000..7336eff7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java @@ -0,0 +1,224 @@ +package de.thedevstack.conversationsplus.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 de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class JingleInbandTransport extends JingleTransport { + + private Account account; + private Jid counterpart; + private int blockSize; + private int bufferSize; + private int seq = 0; + private String sessionId; + + private boolean established = false; + + private boolean connected = true; + + private DownloadableFile file; + private JingleConnection connection; + + private InputStream fileInputStream = null; + private OutputStream fileOutputStream = null; + private long remainingSize = 0; + private long fileSize = 0; + private MessageDigest digest; + + private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; + + private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (connected && packet.getType() == IqPacket.TYPE.RESULT) { + sendNextBlock(); + } + } + }; + + public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) { + this.connection = connection; + this.account = connection.getAccount(); + this.counterpart = connection.getCounterPart(); + 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.connected = true; + 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 = this.fileSize = file.getExpectedSize(); + } catch (final NoSuchAlgorithmException | IOException e) { + callback.onFileTransferAborted(); + } + } + + @Override + public void send(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.remainingSize = this.file.getSize(); + this.fileSize = this.remainingSize; + this.digest = MessageDigest.getInstance("SHA-1"); + this.digest.reset(); + fileInputStream = this.file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + if (this.connected) { + this.sendNextBlock(); + } + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + + @Override + public void disconnect() { + this.connected = false; + if (this.fileOutputStream != null) { + try { + this.fileOutputStream.close(); + } catch (IOException e) { + + } + } + if (this.fileInputStream != null) { + try { + this.fileInputStream.close(); + } catch (IOException e) { + + } + } + } + + 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.remainingSize -= count; + 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++; + connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); + } + } 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); + } else { + connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); + } + } catch (IOException e) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + public void deliverPayload(IqPacket packet, Element payload) { + if (payload.getName().equals("open")) { + if (!established) { + established = true; + connected = true; + this.account.getXmppConnection().sendIqPacket( + packet.generateResponse(IqPacket.TYPE.RESULT), null); + } else { + this.account.getXmppConnection().sendIqPacket( + packet.generateResponse(IqPacket.TYPE.ERROR), null); + } + } else if (connected && payload.getName().equals("data")) { + this.receiveNextBlock(payload.getContent()); + this.account.getXmppConnection().sendIqPacket( + packet.generateResponse(IqPacket.TYPE.RESULT), null); + } else { + // TODO some sort of exception + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java new file mode 100644 index 00000000..a1094dd7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java @@ -0,0 +1,234 @@ +package de.thedevstack.conversationsplus.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 de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.utils.CryptoHelper; + +public class JingleSocks5Transport extends JingleTransport { + private JingleCandidate candidate; + private JingleConnection connection; + 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; + this.connection = jingleConnection; + try { + MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); + StringBuilder destBuilder = new StringBuilder(); + destBuilder.append(jingleConnection.getSessionId()); + if (candidate.isOurs()) { + destBuilder.append(jingleConnection.getAccount().getJid()); + destBuilder.append(jingleConnection.getCounterPart()); + } else { + destBuilder.append(jingleConnection.getCounterPart()); + destBuilder.append(jingleConnection.getAccount().getJid()); + } + 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; + } + long size = file.getSize(); + long transmitted = 0; + int count; + byte[] buffer = new byte[8192]; + while ((count = fileInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + transmitted += count; + connection.updateProgress((int) ((((double) transmitted) / size) * 100)); + } + 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; + } + double size = file.getExpectedSize(); + 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; + } + connection.updateProgress((int) (((size - remainingSize) / size) * 100)); + } + 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.outputStream != null) { + try { + this.outputStream.close(); + } catch (IOException e) { + + } + } + if (this.inputStream != null) { + try { + this.inputStream.close(); + } catch (IOException e) { + + } + } + 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/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleTransport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleTransport.java new file mode 100644 index 00000000..ee0fefff --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleTransport.java @@ -0,0 +1,15 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import de.thedevstack.conversationsplus.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); + + public abstract void disconnect(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnFileTransmissionStatusChanged.java new file mode 100644 index 00000000..86daae81 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnFileTransmissionStatusChanged.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import de.thedevstack.conversationsplus.entities.DownloadableFile; + +public interface OnFileTransmissionStatusChanged { + public void onFileTransmitted(DownloadableFile file); + + public void onFileTransferAborted(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnJinglePacketReceived.java new file mode 100644 index 00000000..d6dda138 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnJinglePacketReceived.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.PacketReceived; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; + +public interface OnJinglePacketReceived extends PacketReceived { + public void onJinglePacketReceived(Account account, JinglePacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnPrimaryCandidateFound.java new file mode 100644 index 00000000..be1f1d02 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnPrimaryCandidateFound.java @@ -0,0 +1,6 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +public interface OnPrimaryCandidateFound { + public void onPrimaryCandidateFound(boolean success, + JingleCandidate canditate); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnTransportConnected.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnTransportConnected.java new file mode 100644 index 00000000..91a79a83 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnTransportConnected.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +public interface OnTransportConnected { + public void failed(); + + public void established(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Content.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Content.java new file mode 100644 index 00000000..40eec6f1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Content.java @@ -0,0 +1,102 @@ +package de.thedevstack.conversationsplus.xmpp.jingle.stanzas; + +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/JinglePacket.java new file mode 100644 index 00000000..db771a0a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/JinglePacket.java @@ -0,0 +1,96 @@ +package de.thedevstack.conversationsplus.xmpp.jingle.stanzas; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.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(final Jid initiator) { + this.jingle.setAttribute("initiator", initiator.toString()); + } + + public boolean isAction(String action) { + return action.equalsIgnoreCase(this.getAction()); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Reason.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Reason.java new file mode 100644 index 00000000..a442d8d3 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Reason.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.xmpp.jingle.stanzas; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Reason extends Element { + private Reason(String name) { + super(name); + } + + public Reason() { + super("reason"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pep/Avatar.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pep/Avatar.java new file mode 100644 index 00000000..e2ce7174 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pep/Avatar.java @@ -0,0 +1,73 @@ +package de.thedevstack.conversationsplus.xmpp.pep; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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 Jid 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/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/AbstractStanza.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/AbstractStanza.java new file mode 100644 index 00000000..d326a214 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/AbstractStanza.java @@ -0,0 +1,54 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class AbstractStanza extends Element { + + protected AbstractStanza(final String name) { + super(name); + } + + public Jid getTo() { + return getAttributeAsJid("to"); + } + + public Jid getFrom() { + return getAttributeAsJid("from"); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setTo(final Jid to) { + if (to != null) { + setAttribute("to", to.toString()); + } + } + + public void setFrom(final Jid from) { + if (from != null) { + setAttribute("from", from.toString()); + } + } + + public void setId(final String id) { + setAttribute("id", id); + } + + public boolean fromServer(final Account account) { + return getFrom() == null + || getFrom().equals(account.getServer()) + || getFrom().equals(account.getJid().toBareJid()) + || getFrom().equals(account.getJid()); + } + + public boolean toServer(final Account account) { + return getTo() == null + || getTo().equals(account.getServer()) + || getTo().equals(account.getJid().toBareJid()) + || getTo().equals(account.getJid()); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacket.java new file mode 100644 index 00000000..fb08556b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacket.java @@ -0,0 +1,63 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +import de.thedevstack.conversationsplus.xml.Element; + +public class IqPacket extends AbstractStanza { + + public static enum TYPE { + ERROR, + SET, + RESULT, + GET, + INVALID + } + + public IqPacket(final TYPE type) { + super("iq"); + if (type != TYPE.INVALID) { + this.setAttribute("type", type.toString().toLowerCase()); + } + } + + public IqPacket() { + super("iq"); + } + + public Element query() { + Element query = findChild("query"); + if (query == null) { + query = addChild("query"); + } + return query; + } + + public Element query(final String xmlns) { + final Element query = query(); + query.setAttribute("xmlns", xmlns); + return query(); + } + + public TYPE getType() { + final String type = getAttribute("type"); + switch (type) { + case "error": + return TYPE.ERROR; + case "result": + return TYPE.RESULT; + case "set": + return TYPE.SET; + case "get": + return TYPE.GET; + default: + return TYPE.INVALID; + } + } + + public IqPacket generateResponse(final TYPE type) { + final IqPacket packet = new IqPacket(type); + packet.setTo(this.getFrom()); + packet.setId(this.getId()); + return packet; + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java new file mode 100644 index 00000000..db1cce54 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java @@ -0,0 +1,69 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +import de.thedevstack.conversationsplus.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(0, 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; + case TYPE_ERROR: + this.setAttribute("type","error"); + 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/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/PresencePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/PresencePacket.java new file mode 100644 index 00000000..dfe40f87 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/PresencePacket.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +public class PresencePacket extends AbstractStanza { + + public PresencePacket() { + super("presence"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/ActivePacket.java new file mode 100644 index 00000000..47538a64 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/ActivePacket.java @@ -0,0 +1,10 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.csi; + +import de.thedevstack.conversationsplus.xmpp.stanzas.AbstractStanza; + +public class ActivePacket extends AbstractStanza { + public ActivePacket() { + super("active"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/InactivePacket.java new file mode 100644 index 00000000..ca5904a5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/InactivePacket.java @@ -0,0 +1,10 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.csi; + +import de.thedevstack.conversationsplus.xmpp.stanzas.AbstractStanza; + +public class InactivePacket extends AbstractStanza { + public InactivePacket() { + super("inactive"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/AckPacket.java new file mode 100644 index 00000000..fe1740da --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/AckPacket.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/EnablePacket.java new file mode 100644 index 00000000..ce416f27 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/EnablePacket.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.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/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/RequestPacket.java new file mode 100644 index 00000000..eb6217ae --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/RequestPacket.java @@ -0,0 +1,12 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.xmpp.stanzas.AbstractStanza; + +public class RequestPacket extends AbstractStanza { + + public RequestPacket(int smVersion) { + super("r"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/ResumePacket.java new file mode 100644 index 00000000..90bf566e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/ResumePacket.java @@ -0,0 +1,14 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.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/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java deleted file mode 100644 index fa9345a2..00000000 --- a/src/main/java/eu/siacs/conversations/Config.java +++ /dev/null @@ -1,81 +0,0 @@ -package eu.siacs.conversations; - -import android.graphics.Bitmap; - -import eu.siacs.conversations.xmpp.chatstate.ChatState; - -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 MINI_GRACE_PERIOD = 750; - - 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 UTF8_EMOTICONS = false; - - public static final int PAGE_SIZE = 50; - public static final int MAX_NUM_PAGES = 3; - - public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; - public static final int REFRESH_UI_INTERVAL = 500; - - public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb - public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance - - public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; - public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2; - public static final int MAM_MAX_MESSAGES = 500; - - public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE; - public static final int TYPING_TIMEOUT = 8; - - public static final String ENABLED_CIPHERS[] = { - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", - - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384", - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", - - "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA", - - // Fallback. - "TLS_RSA_WITH_AES_128_GCM_SHA256", - "TLS_RSA_WITH_AES_128_GCM_SHA384", - "TLS_RSA_WITH_AES_256_GCM_SHA256", - "TLS_RSA_WITH_AES_256_GCM_SHA384", - "TLS_RSA_WITH_AES_128_CBC_SHA256", - "TLS_RSA_WITH_AES_128_CBC_SHA384", - "TLS_RSA_WITH_AES_256_CBC_SHA256", - "TLS_RSA_WITH_AES_256_CBC_SHA384", - "TLS_RSA_WITH_AES_128_CBC_SHA", - "TLS_RSA_WITH_AES_256_CBC_SHA", - }; - - public static final String WEAK_CIPHER_PATTERNS[] = { - "_NULL_", - "_EXPORT_", - "_anon_", - "_RC4_", - "_DES_", - "_MD5", - }; - - private Config() { - - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java deleted file mode 100644 index 20427d7b..00000000 --- a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java +++ /dev/null @@ -1,304 +0,0 @@ -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.entities.Conversation; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; -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.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.InstanceTag; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.FragmenterInstructions; - -public class OtrEngine extends OtrCryptoEngineImpl 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 (final NoSuchAlgorithmException | InvalidKeySpecException e) { - e.printStackTrace(); - } - - } - - @Override - public void askForSecret(SessionID id, InstanceTag instanceTag, String question) { - try { - final Jid jid = Jid.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account,jid); - if (conversation!=null) { - conversation.smp().hint = question; - conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED; - mXmppConnectionService.updateConversationUi(); - } - } catch (InvalidJidException e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString()); - } - } - - @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) { - try { - return getFingerprintRaw(getPublicKey()); - } catch (OtrCryptoException e) { - return null; - } - } - - public PublicKey getPublicKey() { - if (this.keyPair == null) { - return null; - } - return this.keyPair.getPublic(); - } - - @Override - public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { - if (this.keyPair == null) { - KeyPairGenerator kg; - try { - kg = KeyPairGenerator.getInstance("DSA"); - this.keyPair = kg.genKeyPair(); - this.saveKey(); - mXmppConnectionService.databaseBackend.updateAccount(account); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, - "error generating key pair " + e.getMessage()); - } - } - return this.keyPair; - } - - @Override - public String getReplyForUnreadableMessage(SessionID arg0) { - // TODO Auto-generated method stub - return null; - } - - @Override - public OtrPolicy getSessionPolicy(SessionID arg0) { - return otrPolicy; - } - - @Override - public void injectMessage(SessionID session, String body) - throws OtrException { - MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getJid()); - if (session.getUserID().isEmpty()) { - packet.setAttribute("to", session.getAccountID()); - } else { - packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); - } - packet.setBody(body); - packet.addChild("private", "urn:xmpp:carbons:2"); - packet.addChild("no-copy", "urn:xmpp:hints"); - packet.addChild("no-store", "urn:xmpp:hints"); - - try { - Jid jid = Jid.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account,jid); - if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { - if (mXmppConnectionService.sendChatStates()) { - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - } - } - } catch (final InvalidJidException ignored) { - - } - - packet.setType(MessagePacket.TYPE_CHAT); - account.getXmppConnection().sendMessagePacket(packet); - } - - @Override - public void messageFromAnotherInstanceReceived(SessionID session) { - try { - Jid jid = Jid.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account, jid); - String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId(); - if (id != null) { - MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateOtrError(jid,id); - packet.setFrom(account.getJid()); - mXmppConnectionService.sendMessagePacket(account,packet); - Log.d(Config.LOGTAG,packet.toString()); - Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": unreadable OTR message in "+conversation.getName()); - } - } catch (InvalidJidException e) { - return; - } - } - - @Override - public void multipleInstancesDetected(SessionID arg0) { - // TODO Auto-generated method stub - - } - - @Override - public void requireEncryptedMessage(SessionID arg0, String arg1) - throws OtrException { - // TODO Auto-generated method stub - - } - - @Override - public void showError(SessionID arg0, String arg1) throws OtrException { - Log.d(Config.LOGTAG,"show error"); - } - - @Override - public void smpAborted(SessionID id) throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - private void setSmpStatus(SessionID id, int status) { - try { - final Jid jid = Jid.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account,jid); - if (conversation!=null) { - conversation.smp().status = status; - mXmppConnectionService.updateConversationUi(); - } - } catch (final InvalidJidException ignored) { - - } - } - - @Override - public void smpError(SessionID id, int arg1, boolean arg2) - throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - @Override - public void unencryptedMessageReceived(SessionID arg0, String arg1) - throws OtrException { - throw new OtrException(new Exception("unencrypted message received")); - } - - @Override - public void unreadableMessageReceived(SessionID arg0) throws OtrException { - Log.d(Config.LOGTAG,"unreadable message received"); - throw new OtrException(new Exception("unreadable message received")); - } - - @Override - public void unverify(SessionID id, String arg1) { - setSmpStatus(id, Conversation.Smp.STATUS_FAILED); - } - - @Override - public void verify(SessionID id, String fingerprint, boolean approved) { - Log.d(Config.LOGTAG,"OtrEngine.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")"); - try { - final Jid jid = Jid.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account,jid); - if (conversation!=null) { - if (approved) { - conversation.getContact().addOtrFingerprint(fingerprint); - } - conversation.smp().hint = null; - conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final InvalidJidException ignored) { - } - } - - @Override - public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) { - return null; - } - -} diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java deleted file mode 100644 index 3dc3fd34..00000000 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ /dev/null @@ -1,369 +0,0 @@ -package eu.siacs.conversations.crypto; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; - -import org.openintents.openpgp.OpenPgpSignatureResult; -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; - -import de.tzur.conversations.Settings; -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.http.HttpConnectionManager; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.UiCallback; -import android.app.PendingIntent; -import android.content.Intent; -import android.net.Uri; - -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) { - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message - .getConversation().getAccount().getJid().toBareJid().toString()); - 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); - final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); - if (message.trusted() - && Settings.DOWNLOAD_IMAGE_LINKS - && mXmppConnectionService.isDownloadAllowedInConnection() - && message.bodyContainsDownloadable() - && manager.getAutoAcceptFileSize() > 0) { - manager.createNewConnection(message); - } - 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: - callback.error(R.string.openpgp_error, message); - } - } - }); - } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { - try { - final DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); - final DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); - outputFile.getParentFile().mkdirs(); - 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: - URL url = message.getImageParams().url; - mXmppConnectionService.getFileBackend().updateFileParams(message,url); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - PgpEngine.this.mXmppConnectionService - .updateMessage(message); - inputFile.delete(); - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(Uri.fromFile(outputFile)); - mXmppConnectionService.sendBroadcast(intent); - 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); - } - } - }); - } catch (final 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().toBareJid().toString()); - - 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]); - } - } - 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 || message.getType() == Message.TYPE_FILE) { - try { - DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); - DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); - outputFile.getParentFile().mkdirs(); - 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 (final IOException e) { - callback.error(R.string.openpgp_error, message); - } - } - } - - public long fetchKeyId(Account account, String status, String signature) { - if ((signature == null) || (api == null)) { - return 0; - } - if (status == null) { - status = ""; - } - final 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().toBareJid().toString()); - 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: - 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().toBareJid().toString()); - 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); - } - } - }); - } - - 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().toBareJid().toString()); - 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); - } - } - }); - } - - 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().toBareJid().toString()); - 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().toBareJid().toString()); - Intent result = api.executeApi(params, null, null); - return (PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java deleted file mode 100644 index 8b16215b..00000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ /dev/null @@ -1,91 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; - -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 eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.TagWriter; - -public class DigestMd5 extends SaslMechanism { - public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - - @Override - public int getPriority() { - return 10; - } - - @Override - public String getMechanism() { - return "DIGEST-MD5"; - } - - private State state = State.INITIAL; - - @Override - public String getResponse(final String challenge) throws AuthenticationException { - switch (state) { - case INITIAL: - state = State.RESPONSE_SENT; - final String encodedResponse; - try { - final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); - String nonce = ""; - for (final String token : tokenizer) { - final String[] parts = token.split("=", 2); - if (parts[0].equals("nonce")) { - nonce = parts[1].replace("\"", ""); - } else if (parts[0].equals("rspauth")) { - return ""; - } - } - final String digestUri = "xmpp/" + account.getServer(); - final String nonceCount = "00000001"; - final String x = account.getUsername() + ":" + account.getServer() + ":" - + account.getPassword(); - final MessageDigest md = MessageDigest.getInstance("MD5"); - final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = new BigInteger(100, rng).toString(32); - final byte[] a1 = CryptoHelper.concatenateByteArrays(y, - (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); - final String a2 = "AUTHENTICATE:" + digestUri; - final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); - final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset - .defaultCharset()))); - final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce - + ":auth:" + ha2; - final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset - .defaultCharset()))); - final String saslString = "username=\"" + account.getUsername() - + "\",realm=\"" + account.getServer() + "\",nonce=\"" - + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount - + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" - + response + ",charset=utf-8"; - encodedResponse = Base64.encodeToString( - saslString.getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); - } catch (final NoSuchAlgorithmException e) { - throw new AuthenticationException(e); - } - - return encodedResponse; - case RESPONSE_SENT: - state = State.VALID_SERVER_RESPONSE; - break; - case VALID_SERVER_RESPONSE: - if (challenge==null) { - return null; //everything is fine - } - default: - throw new InvalidStateException(state); - } - return null; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java deleted file mode 100644 index 40a55151..00000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ /dev/null @@ -1,30 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; - -import java.nio.charset.Charset; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; - -public class Plain extends SaslMechanism { - public Plain(final TagWriter tagWriter, final Account account) { - super(tagWriter, account, null); - } - - @Override - public int getPriority() { - return 10; - } - - @Override - public String getMechanism() { - return "PLAIN"; - } - - @Override - public String getClientFirstMessage() { - final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword(); - return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java deleted file mode 100644 index 14d8b944..00000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ /dev/null @@ -1,62 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import java.security.SecureRandom; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; - -public abstract class SaslMechanism { - - final protected TagWriter tagWriter; - final protected Account account; - final protected SecureRandom rng; - - protected static enum State { - INITIAL, - AUTH_TEXT_SENT, - RESPONSE_SENT, - VALID_SERVER_RESPONSE, - } - - public static class AuthenticationException extends Exception { - public AuthenticationException(final String message) { - super(message); - } - - public AuthenticationException(final Exception inner) { - super(inner); - } - } - - public static class InvalidStateException extends AuthenticationException { - public InvalidStateException(final String message) { - super(message); - } - - public InvalidStateException(final State state) { - this("Invalid state: " + state.toString()); - } - } - - public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - this.tagWriter = tagWriter; - this.account = account; - this.rng = rng; - } - - /** - * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another - * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade - * attacks). - * @return An arbitrary int representing the priority - */ - public abstract int getPriority(); - - public abstract String getMechanism(); - public String getClientFirstMessage() { - return ""; - } - public String getResponse(final String challenge) throws AuthenticationException { - return ""; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java deleted file mode 100644 index 10cd3167..00000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ /dev/null @@ -1,232 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; -import android.util.LruCache; - -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA1Digest; -import org.bouncycastle.crypto.macs.HMac; -import org.bouncycastle.crypto.params.KeyParameter; - -import java.math.BigInteger; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.SecureRandom; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.TagWriter; - -public class ScramSha1 extends SaslMechanism { - // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. - final private static String GS2_HEADER = "n,,"; - private String clientFirstMessageBare; - private byte[] serverFirstMessage; - final private String clientNonce; - private byte[] serverSignature = null; - private static HMac HMAC; - private static Digest DIGEST; - private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); - private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); - - public static class KeyPair { - final public byte[] clientKey; - final public byte[] serverKey; - - public KeyPair(final byte[] clientKey, final byte[] serverKey) { - this.clientKey = clientKey; - this.serverKey = serverKey; - } - } - - private static final LruCache CACHE; - - static { - DIGEST = new SHA1Digest(); - HMAC = new HMac(new SHA1Digest()); - CACHE = new LruCache(10) { - protected KeyPair create(final String k) { - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". - // Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()' - // is applied to prevent commas in the strings breaking things. - final String[] kparts = k.split(",", 4); - try { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), - Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); - - return new KeyPair(clientKey, serverKey); - } catch (final InvalidKeyException | NumberFormatException e) { - return null; - } - } - }; - } - - private State state = State.INITIAL; - - public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - - // This nonce should be different for each authentication attempt. - clientNonce = new BigInteger(100, this.rng).toString(32); - clientFirstMessageBare = ""; - } - - @Override - public int getPriority() { - return 20; - } - - @Override - public String getMechanism() { - return "SCRAM-SHA-1"; - } - - @Override - public String getClientFirstMessage() { - if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { - clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + - ",r=" + this.clientNonce; - state = State.AUTH_TEXT_SENT; - } - return Base64.encodeToString( - (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); - } - - @Override - public String getResponse(final String challenge) throws AuthenticationException { - switch (state) { - case AUTH_TEXT_SENT: - serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); - final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); - String nonce = ""; - int iterationCount = -1; - String salt = ""; - for (final String token : tokenizer) { - if (token.charAt(1) == '=') { - switch (token.charAt(0)) { - case 'i': - try { - iterationCount = Integer.parseInt(token.substring(2)); - } catch (final NumberFormatException e) { - throw new AuthenticationException(e); - } - break; - case 's': - salt = token.substring(2); - break; - case 'r': - nonce = token.substring(2); - break; - case 'm': - /* - * RFC 5802: - * m: This attribute is reserved for future extensibility. In this - * version of SCRAM, its presence in a client or a server message - * MUST cause authentication failure when the attribute is parsed by - * the other end. - */ - throw new AuthenticationException("Server sent reserved token: `m'"); - } - } - } - - if (iterationCount < 0) { - throw new AuthenticationException("Server did not send iteration count"); - } - if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { - throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); - } - if (salt.isEmpty()) { - throw new AuthenticationException("Server sent empty salt"); - } - - final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( - GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; - final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' - + clientFinalMessageWithoutProof).getBytes(); - - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". - final KeyPair keys = CACHE.get( - CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," - + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," - + CryptoHelper.bytesToHex(salt.getBytes()) + "," - + String.valueOf(iterationCount) - ); - if (keys == null) { - throw new AuthenticationException("Invalid keys generated"); - } - final byte[] clientSignature; - try { - serverSignature = hmac(keys.serverKey, authMessage); - final byte[] storedKey = digest(keys.clientKey); - - clientSignature = hmac(storedKey, authMessage); - - } catch (final InvalidKeyException e) { - throw new AuthenticationException(e); - } - - final byte[] clientProof = new byte[keys.clientKey.length]; - - for (int i = 0; i < clientProof.length; i++) { - clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); - } - - - final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + - Base64.encodeToString(clientProof, Base64.NO_WRAP); - state = State.RESPONSE_SENT; - return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); - case RESPONSE_SENT: - final String clientCalculatedServerFinalMessage = "v=" + - Base64.encodeToString(serverSignature, Base64.NO_WRAP); - if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { - throw new AuthenticationException("Server final message does not match calculated final message"); - } - state = State.VALID_SERVER_RESPONSE; - return ""; - default: - throw new InvalidStateException(state); - } - } - - public static synchronized byte[] hmac(final byte[] key, final byte[] input) - throws InvalidKeyException { - HMAC.init(new KeyParameter(key)); - HMAC.update(input, 0, input.length); - final byte[] out = new byte[HMAC.getMacSize()]; - HMAC.doFinal(out, 0); - return out; - } - - public static synchronized byte[] digest(byte[] bytes) { - DIGEST.reset(); - DIGEST.update(bytes, 0, bytes.length); - final byte[] out = new byte[DIGEST.getDigestSize()]; - DIGEST.doFinal(out, 0); - return out; - } - - /* - * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the - * pseudorandom function (PRF) and with dkLen == output length of - * HMAC() == output length of H(). - */ - private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) - throws InvalidKeyException { - byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); - byte[] out = u.clone(); - for (int i = 1; i < iterations; i++) { - u = hmac(key, u); - for (int j = 0; j < u.length; j++) { - out[j] ^= u[j]; - } - } - return out; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java deleted file mode 100644 index e37e0fa7..00000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java +++ /dev/null @@ -1,78 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - -/** - * A tokenizer for GS2 header strings - */ -public final class Tokenizer implements Iterator, Iterable { - private final List parts; - private int index; - - public Tokenizer(final byte[] challenge) { - final String challengeString = new String(challenge); - parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); - // Trim parts. - for (int i = 0; i < parts.size(); i++) { - parts.set(i, parts.get(i).trim()); - } - index = 0; - } - - /** - * Returns true if there is at least one more element, false otherwise. - * - * @see #next - */ - @Override - public boolean hasNext() { - return parts.size() != index + 1; - } - - /** - * Returns the next object and advances the iterator. - * - * @return the next object. - * @throws java.util.NoSuchElementException if there are no more elements. - * @see #hasNext - */ - @Override - public String next() { - if (hasNext()) { - return parts.get(index++); - } else { - throw new NoSuchElementException("No such element. Size is: " + parts.size()); - } - } - - /** - * Removes the last object returned by {@code next} from the collection. - * This method can only be called once between each call to {@code next}. - * - * @throws UnsupportedOperationException if removing is not supported by the collection being - * iterated. - * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has - * already been called after the last call to {@code next}. - */ - @Override - public void remove() { - if(index <= 0) { - throw new IllegalStateException("You can't delete an element before first next() method call"); - } - parts.remove(--index); - } - - /** - * Returns an {@link java.util.Iterator} for the elements in this object. - * - * @return An {@code Iterator} instance. - */ - @Override - public Iterator iterator() { - return parts.iterator(); - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java deleted file mode 100644 index 957b0a14..00000000 --- a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java +++ /dev/null @@ -1,20 +0,0 @@ -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/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java deleted file mode 100644 index 2bc2c954..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ /dev/null @@ -1,411 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; -import android.os.SystemClock; - -import net.java.otr4j.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.security.PublicKey; -import java.security.interfaces.DSAPublicKey; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CopyOnWriteArraySet; - -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 eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -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 String PINNED_MECHANISM_KEY = "pinned_mechanism"; - - 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 enum State { - DISABLED, - OFFLINE, - CONNECTING, - ONLINE, - NO_INTERNET, - UNAUTHORIZED(true), - SERVER_NOT_FOUND(true), - REGISTRATION_FAILED(true), - REGISTRATION_CONFLICT(true), - REGISTRATION_SUCCESSFUL, - REGISTRATION_NOT_SUPPORTED(true), - SECURITY_ERROR(true), - INCOMPATIBLE_SERVER(true); - - private final boolean isError; - - public boolean isError() { - return this.isError; - } - - private State(final boolean isError) { - this.isError = isError; - } - - private State() { - this(false); - } - - public int getReadableId() { - switch (this) { - case DISABLED: - return R.string.account_status_disabled; - case ONLINE: - return R.string.account_status_online; - case CONNECTING: - return R.string.account_status_connecting; - case OFFLINE: - return R.string.account_status_offline; - case UNAUTHORIZED: - return R.string.account_status_unauthorized; - case SERVER_NOT_FOUND: - return R.string.account_status_not_found; - case NO_INTERNET: - return R.string.account_status_no_internet; - case REGISTRATION_FAILED: - return R.string.account_status_regis_fail; - case REGISTRATION_CONFLICT: - return R.string.account_status_regis_conflict; - case REGISTRATION_SUCCESSFUL: - return R.string.account_status_regis_success; - case REGISTRATION_NOT_SUPPORTED: - return R.string.account_status_regis_not_sup; - case SECURITY_ERROR: - return R.string.account_status_security_error; - case INCOMPATIBLE_SERVER: - return R.string.account_status_incompatible_server; - default: - return R.string.account_status_unknown; - } - } - } - - public List pendingConferenceJoins = new CopyOnWriteArrayList<>(); - public List pendingConferenceLeaves = new CopyOnWriteArrayList<>(); - protected Jid jid; - protected String password; - protected int options = 0; - protected String rosterVersion; - protected State status = State.OFFLINE; - protected JSONObject keys = new JSONObject(); - protected String avatar; - protected boolean online = false; - private OtrEngine otrEngine = null; - private XmppConnection xmppConnection = null; - private long mEndGracePeriod = 0L; - private String otrFingerprint; - private final Roster roster = new Roster(this); - private List bookmarks = new CopyOnWriteArrayList<>(); - private final Collection blocklist = new CopyOnWriteArraySet<>(); - - public Account() { - this.uuid = "0"; - } - - public Account(final Jid jid, final String password) { - this(java.util.UUID.randomUUID().toString(), jid, - password, 0, null, "", null); - } - - public Account(final String uuid, final Jid jid, - final String password, final int options, final String rosterVersion, final String keys, - final String avatar) { - this.uuid = uuid; - this.jid = jid; - if (jid.isBareJid()) { - this.setResource("mobile"); - } - this.password = password; - this.options = options; - this.rosterVersion = rosterVersion; - try { - this.keys = new JSONObject(keys); - } catch (final JSONException ignored) { - this.keys = new JSONObject(); - } - this.avatar = avatar; - } - - public static Account fromCursor(final Cursor cursor) { - Jid jid = null; - try { - jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)), - cursor.getString(cursor.getColumnIndex(SERVER)), "mobile"); - } catch (final InvalidJidException ignored) { - } - return new Account(cursor.getString(cursor.getColumnIndex(UUID)), - jid, - 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 boolean isOptionSet(final int option) { - return ((options & (1 << option)) != 0); - } - - public void setOption(final int option, final boolean value) { - if (value) { - this.options |= 1 << option; - } else { - this.options &= ~(1 << option); - } - } - - public String getUsername() { - return jid.getLocalpart(); - } - - public void setUsername(final String username) throws InvalidJidException { - jid = Jid.fromParts(username, jid.getDomainpart(), jid.getResourcepart()); - } - - public Jid getServer() { - return jid.toDomainJid(); - } - - public void setServer(final String server) throws InvalidJidException { - jid = Jid.fromParts(jid.getLocalpart(), server, jid.getResourcepart()); - } - - public String getPassword() { - return password; - } - - public void setPassword(final String password) { - this.password = password; - } - - public State getStatus() { - if (isOptionSet(OPTION_DISABLED)) { - return State.DISABLED; - } else { - return this.status; - } - } - - public void setStatus(final State status) { - this.status = status; - } - - public boolean errorStatus() { - return getStatus().isError(); - } - - public boolean hasErrorStatus() { - return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 2; - } - - public String getResource() { - return jid.getResourcepart(); - } - - public void setResource(final String resource) { - try { - jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource); - } catch (final InvalidJidException ignored) { - } - } - - public Jid getJid() { - return jid; - } - - public JSONObject getKeys() { - return keys; - } - - public boolean setKey(final String keyName, final String keyValue) { - try { - this.keys.put(keyName, keyValue); - return true; - } catch (final JSONException e) { - return false; - } - } - - @Override - public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(USERNAME, jid.getLocalpart()); - values.put(SERVER, jid.getDomainpart()); - 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 void initOtrEngine(final XmppConnectionService context) { - this.otrEngine = new OtrEngine(context, this); - } - - public OtrEngine getOtrEngine() { - return this.otrEngine; - } - - public XmppConnection getXmppConnection() { - return this.xmppConnection; - } - - public void setXmppConnection(final XmppConnection connection) { - this.xmppConnection = connection; - } - - public String getOtrFingerprint() { - if (this.otrFingerprint == null) { - try { - if (this.otrEngine == null) { - return null; - } - final PublicKey publicKey = this.otrEngine.getPublicKey(); - if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { - return null; - } - this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey); - return this.otrFingerprint; - } catch (final OtrCryptoException ignored) { - return null; - } - } else { - return this.otrFingerprint; - } - } - - public String getRosterVersion() { - if (this.rosterVersion == null) { - return ""; - } else { - return this.rosterVersion; - } - } - - public void setRosterVersion(final String version) { - this.rosterVersion = version; - } - - public int countPresences() { - return this.getRoster().getContact(this.getJid().toBareJid()).getPresences().size(); - } - - public String getPgpSignature() { - if (keys.has("pgp_signature")) { - try { - return keys.getString("pgp_signature"); - } catch (final JSONException e) { - return null; - } - } else { - return null; - } - } - - public Roster getRoster() { - return this.roster; - } - - public List getBookmarks() { - return this.bookmarks; - } - - public void setBookmarks(final List bookmarks) { - this.bookmarks = bookmarks; - } - - public boolean hasBookmarkFor(final Jid conferenceJid) { - for (final Bookmark bookmark : this.bookmarks) { - final Jid jid = bookmark.getJid(); - if (jid != null && jid.equals(conferenceJid.toBareJid())) { - return true; - } - } - return false; - } - - public boolean setAvatar(final 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 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; - } - - public String getShareableUri() { - final String fingerprint = this.getOtrFingerprint(); - if (fingerprint != null) { - return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint; - } else { - return "xmpp:" + this.getJid().toBareJid().toString(); - } - } - - public boolean isBlocked(final ListItem contact) { - final Jid jid = contact.getJid(); - return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid())); - } - - public boolean isBlocked(final Jid jid) { - return jid != null && blocklist.contains(jid.toBareJid()); - } - - public Collection getBlocklist() { - return this.blocklist; - } - - public void clearBlocklist() { - getBlocklist().clear(); - } - - public boolean isOnlineAndConnected() { - return this.getStatus() == State.ONLINE && this.getXmppConnection() != null; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Blockable.java b/src/main/java/eu/siacs/conversations/entities/Blockable.java deleted file mode 100644 index dbcd55c4..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Blockable.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.siacs.conversations.entities; - -import eu.siacs.conversations.xmpp.jid.Jid; - -public interface Blockable { - public boolean isBlocked(); - public boolean isDomainBlocked(); - public Jid getBlockedJid(); - public Jid getJid(); - public Account getAccount(); -} diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java deleted file mode 100644 index f81f1a87..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ /dev/null @@ -1,160 +0,0 @@ -package eu.siacs.conversations.entities; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class Bookmark extends Element implements ListItem { - - private Account account; - private Conversation mJoinedConversation; - - public Bookmark(final Account account, final Jid jid) { - super("conference"); - this.setAttribute("jid", jid.toString()); - 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"); - } - } - - @Override - public int compareTo(final 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().getLocalpart(); - } - } - - @Override - public Jid getJid() { - return this.getAttributeAsJid("jid"); - } - - @Override - public List getTags() { - ArrayList tags = new ArrayList(); - for (Element element : getChildren()) { - if (element.getName().equals("group") && element.getContent() != null) { - String group = element.getContent(); - tags.add(new Tag(group, UIHelper.getColorForName(group))); - } - } - return tags; - } - - public String getNick() { - Element nick = this.findChild("nick"); - if (nick != null) { - return nick.getContent(); - } else { - return null; - } - } - - public void setNick(String nick) { - Element element = this.findChild("nick"); - if (element == null) { - element = this.addChild("nick"); - } - element.setContent(nick); - } - - public boolean autojoin() { - return this.getAttributeAsBoolean("autojoin"); - } - - public String getPassword() { - Element password = this.findChild("password"); - if (password != null) { - return password.getContent(); - } else { - return null; - } - } - - public void setPassword(String password) { - Element element = this.findChild("password"); - if (element != null) { - element.setContent(password); - } - } - - public boolean match(String needle) { - if (needle == null) { - return true; - } - needle = needle.toLowerCase(Locale.US); - final Jid jid = getJid(); - return (jid != null && jid.toString().contains(needle)) || - getDisplayName().toLowerCase(Locale.US).contains(needle) || - matchInTag(needle); - } - - private boolean matchInTag(String needle) { - needle = needle.toLowerCase(Locale.US); - for (Tag tag : getTags()) { - if (tag.getName().toLowerCase(Locale.US).contains(needle)) { - return true; - } - } - return false; - } - - public Account getAccount() { - return this.account; - } - - public Conversation getConversation() { - return this.mJoinedConversation; - } - - public void setConversation(Conversation conversation) { - this.mJoinedConversation = conversation; - } - - public String getName() { - return this.getAttribute("name"); - } - - public void setName(String name) { - this.name = name; - } - - public void unregisterConversation() { - if (this.mJoinedConversation != null) { - this.mJoinedConversation.deregisterWithBookmark(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java deleted file mode 100644 index cef03ebe..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ /dev/null @@ -1,505 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class Contact implements ListItem, Blockable { - 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"; - public static final String LAST_PRESENCE = "last_presence"; - public static final String LAST_TIME = "last_time"; - public static final String GROUPS = "groups"; - public Lastseen lastseen = new Lastseen(); - protected String accountUuid; - protected String systemName; - protected String serverName; - protected String presenceName; - protected Jid jid; - protected int subscription = 0; - protected String systemAccount; - protected String photoUri; - protected String avatar; - protected JSONObject keys = new JSONObject(); - protected JSONArray groups = new JSONArray(); - protected Presences presences = new Presences(); - protected Account account; - - public Contact(final String account, final String systemName, final String serverName, - final Jid jid, final int subscription, final String photoUri, - final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) { - this.accountUuid = account; - this.systemName = systemName; - this.serverName = serverName; - this.jid = jid; - this.subscription = subscription; - this.photoUri = photoUri; - this.systemAccount = systemAccount; - try { - this.keys = (keys == null ? new JSONObject("") : new JSONObject(keys)); - } catch (JSONException e) { - this.keys = new JSONObject(); - } - this.avatar = avatar; - try { - this.groups = (groups == null ? new JSONArray() : new JSONArray(groups)); - } catch (JSONException e) { - this.groups = new JSONArray(); - } - this.lastseen = lastseen; - } - - public Contact(final Jid jid) { - this.jid = jid; - } - - public static Contact fromCursor(final Cursor cursor) { - final Lastseen lastseen = new Lastseen( - cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)), - cursor.getLong(cursor.getColumnIndex(LAST_TIME))); - final Jid jid; - try { - jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true); - } catch (final InvalidJidException e) { - // TODO: Borked DB... handle this somehow? - return null; - } - return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), - cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), - cursor.getString(cursor.getColumnIndex(SERVERNAME)), - 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)), - lastseen, - cursor.getString(cursor.getColumnIndex(GROUPS))); - } - - 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 if (jid.hasLocalpart()) { - return jid.getLocalpart(); - } else { - return jid.getDomainpart(); - } - } - - public String getProfilePhoto() { - return this.photoUri; - } - - public Jid getJid() { - return jid; - } - - @Override - public List getTags() { - final ArrayList tags = new ArrayList<>(); - for (final String group : getGroups()) { - tags.add(new Tag(group, UIHelper.getColorForName(group))); - } - switch (getMostAvailableStatus()) { - case Presences.CHAT: - case Presences.ONLINE: - tags.add(new Tag("online", 0xff259b24)); - break; - case Presences.AWAY: - tags.add(new Tag("away", 0xffff9800)); - break; - case Presences.XA: - tags.add(new Tag("not available", 0xffe51c23)); - break; - case Presences.DND: - tags.add(new Tag("dnd", 0xffe51c23)); - break; - } - if (isBlocked()) { - tags.add(new Tag("blocked", 0xff2e2f3b)); - } - return tags; - } - - public boolean match(String needle) { - if (needle == null || needle.isEmpty()) { - return true; - } - needle = needle.toLowerCase(Locale.US).trim(); - String[] parts = needle.split("\\s+"); - if (parts.length > 1) { - for(int i = 0; i < parts.length; ++i) { - if (!match(parts[i])) { - return false; - } - } - return true; - } else { - return jid.toString().contains(needle) || - getDisplayName().toLowerCase(Locale.US).contains(needle) || - matchInTag(needle); - } - } - - private boolean matchInTag(String needle) { - needle = needle.toLowerCase(Locale.US); - for (Tag tag : getTags()) { - if (tag.getName().toLowerCase(Locale.US).contains(needle)) { - return true; - } - } - return false; - } - - public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(ACCOUNT, accountUuid); - values.put(SYSTEMNAME, systemName); - values.put(SERVERNAME, serverName); - values.put(JID, jid.toString()); - values.put(OPTIONS, subscription); - values.put(SYSTEMACCOUNT, systemAccount); - values.put(PHOTOURI, photoUri); - values.put(KEYS, keys.toString()); - values.put(AVATAR, avatar); - values.put(LAST_PRESENCE, lastseen.presence); - values.put(LAST_TIME, lastseen.time); - values.put(GROUPS, groups.toString()); - return values; - } - - public int getSubscription() { - return this.subscription; - } - - public Account getAccount() { - return this.account; - } - - public void setAccount(Account account) { - this.account = account; - this.accountUuid = account.getUuid(); - } - - public Presences getPresences() { - return this.presences; - } - - public void setPresences(Presences pres) { - this.presences = pres; - } - - public void updatePresence(final String resource, final int status) { - this.presences.updatePresence(resource, status); - } - - public void removePresence(final 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 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 void setSystemAccount(String account) { - this.systemAccount = account; - } - - public List getGroups() { - ArrayList groups = new ArrayList(); - for (int i = 0; i < this.groups.length(); ++i) { - try { - groups.add(this.groups.getString(i)); - } catch (final JSONException ignored) { - } - } - return groups; - } - - public ArrayList getOtrFingerprints() { - final ArrayList fingerprints = new ArrayList(); - try { - if (this.keys.has("otr_fingerprints")) { - final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); - for (int i = 0; i < prints.length(); ++i) { - final String print = prints.isNull(i) ? null : prints.getString(i); - if (print != null && !print.isEmpty()) { - fingerprints.add(prints.getString(i)); - } - } - } - } catch (final JSONException ignored) { - - } - return fingerprints; - } - - public boolean addOtrFingerprint(String print) { - if (getOtrFingerprints().contains(print)) { - return false; - } - 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); - return true; - } catch (final JSONException ignored) { - return false; - } - } - - 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 setPgpKeyId(long keyId) { - try { - this.keys.put("pgp_keyid", keyId); - } catch (final JSONException ignored) { - - } - } - - 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) { - switch (subscription) { - case "to": - this.resetOption(Options.FROM); - this.setOption(Options.TO); - break; - case "from": - this.resetOption(Options.TO); - this.setOption(Options.FROM); - this.resetOption(Options.PREEMPTIVE_GRANT); - break; - case "both": - this.setOption(Options.TO); - this.setOption(Options.FROM); - this.resetOption(Options.PREEMPTIVE_GRANT); - break; - case "none": - this.resetOption(Options.FROM); - this.resetOption(Options.TO); - break; - } - } - - // 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 void parseGroupsFromElement(Element item) { - this.groups = new JSONArray(); - for (Element element : item.getChildren()) { - if (element.getName().equals("group") && element.getContent() != null) { - this.groups.put(element.getContent()); - } - } - } - - public Element asElement() { - final Element item = new Element("item"); - item.setAttribute("jid", this.jid.toString()); - if (this.serverName != null) { - item.setAttribute("name", this.serverName); - } - for (String group : getGroups()) { - item.addChild("group").setContent(group); - } - return item; - } - - @Override - public int compareTo(final ListItem another) { - return this.getDisplayName().compareToIgnoreCase( - another.getDisplayName()); - } - - public Jid getServer() { - return getJid().toDomainJid(); - } - - 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); - } - - public String getShareableUri() { - if (getOtrFingerprints().size() >= 1) { - String otr = getOtrFingerprints().get(0); - return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr; - } else { - return "xmpp:" + getJid().toBareJid().toString(); - } - } - - @Override - public boolean isBlocked() { - return getAccount().isBlocked(this); - } - - @Override - public boolean isDomainBlocked() { - return getAccount().isBlocked(this.getJid().toDomainJid()); - } - - @Override - public Jid getBlockedJid() { - if (isDomainBlocked()) { - return getJid().toDomainJid(); - } else { - return getJid(); - } - } - - public static class Lastseen { - public long time; - public String presence; - - public Lastseen() { - this(null, 0); - } - - public Lastseen(final String presence, final long time) { - this.presence = presence; - this.time = time; - } - } - - public final 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; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java deleted file mode 100644 index bfee5007..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ /dev/null @@ -1,770 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; - -import net.java.otr4j.OtrException; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.SessionImpl; -import net.java.otr4j.session.SessionStatus; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.security.interfaces.DSAPublicKey; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class Conversation extends AbstractEntity implements Blockable { - 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"; - public static final String ATTRIBUTE_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted"; - - private String name; - private String contactUuid; - private String accountUuid; - private Jid contactJid; - private int status; - private long created; - private int mode; - - private JSONObject attributes = new JSONObject(); - - private Jid nextCounterpart; - - protected final ArrayList messages = new ArrayList<>(); - protected Account account = null; - - private transient SessionImpl otrSession; - - private transient String otrFingerprint = null; - private Smp mSmp = new Smp(); - - private String nextMessage; - - private transient MucOptions mucOptions = null; - - private byte[] symmetricKey; - - private Bookmark bookmark; - - private boolean messagesLeftOnServer = true; - private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE; - private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; - private String mLastReceivedOtrMessageId = null; - - public boolean hasMessagesLeftOnServer() { - return messagesLeftOnServer; - } - - public void setHasMessagesLeftOnServer(boolean value) { - this.messagesLeftOnServer = value; - } - - public Message findUnsentMessageWithUuid(String uuid) { - synchronized(this.messages) { - for (final Message message : this.messages) { - final int s = message.getStatus(); - if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { - return message; - } - } - } - return null; - } - - public void findWaitingMessages(OnMessageFound onMessageFound) { - synchronized (this.messages) { - for(Message message : this.messages) { - if (message.getStatus() == Message.STATUS_WAITING) { - onMessageFound.onMessageFound(message); - } - } - } - } - - public void findMessagesWithFiles(final OnMessageFound onMessageFound) { - synchronized (this.messages) { - for (final Message message : this.messages) { - if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) - && message.getEncryption() != Message.ENCRYPTION_PGP) { - onMessageFound.onMessageFound(message); - } - } - } - } - - public Message findMessageWithFileAndUuid(final String uuid) { - synchronized (this.messages) { - for (final Message message : this.messages) { - if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) - && message.getEncryption() != Message.ENCRYPTION_PGP - && message.getUuid().equals(uuid)) { - return message; - } - } - } - return null; - } - - public void clearMessages() { - synchronized (this.messages) { - this.messages.clear(); - } - } - - public boolean setIncomingChatState(ChatState state) { - if (this.mIncomingChatState == state) { - return false; - } - this.mIncomingChatState = state; - return true; - } - - public ChatState getIncomingChatState() { - return this.mIncomingChatState; - } - - public boolean setOutgoingChatState(ChatState state) { - if (mode == MODE_MULTI) { - return false; - } - if (this.mOutgoingChatState != state) { - this.mOutgoingChatState = state; - return true; - } else { - return false; - } - } - - public ChatState getOutgoingChatState() { - return this.mOutgoingChatState; - } - - public void trim() { - synchronized (this.messages) { - final int size = messages.size(); - final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; - if (size > maxsize) { - this.messages.subList(0, size - maxsize).clear(); - } - } - } - - public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { - synchronized (this.messages) { - for (Message message : this.messages) { - if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) - && (message.getEncryption() == Message.ENCRYPTION_OTR)) { - onMessageFound.onMessageFound(message); - } - } - } - } - - public void findUnsentTextMessages(OnMessageFound onMessageFound) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (message.getType() != Message.TYPE_IMAGE - && message.getStatus() == Message.STATUS_UNSEND) { - onMessageFound.onMessageFound(message); - } - } - } - } - - public Message findSentMessageWithUuid(String uuid) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (uuid.equals(message.getUuid()) - || (message.getStatus() >= Message.STATUS_SEND && uuid - .equals(message.getRemoteMsgId()))) { - return message; - } - } - } - return null; - } - - public void populateWithMessages(final List messages) { - synchronized (this.messages) { - messages.clear(); - messages.addAll(this.messages); - } - } - - @Override - public boolean isBlocked() { - return getContact().isBlocked(); - } - - @Override - public boolean isDomainBlocked() { - return getContact().isDomainBlocked(); - } - - @Override - public Jid getBlockedJid() { - return getContact().getBlockedJid(); - } - - public String getLastReceivedOtrMessageId() { - return this.mLastReceivedOtrMessageId; - } - - public void setLastReceivedOtrMessageId(String id) { - this.mLastReceivedOtrMessageId = id; - } - - - public interface OnMessageFound { - public void onMessageFound(final Message message); - } - - public Conversation(final String name, final Account account, final Jid contactJid, - final int mode) { - this(java.util.UUID.randomUUID().toString(), name, null, account - .getUuid(), contactJid, System.currentTimeMillis(), - STATUS_AVAILABLE, mode, ""); - this.account = account; - } - - public Conversation(final String uuid, final String name, final String contactUuid, - final String accountUuid, final Jid contactJid, final long created, final int status, - final int mode, final 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 { - this.attributes = new JSONObject(attributes == null ? "" : attributes); - } catch (JSONException e) { - this.attributes = new JSONObject(); - } - } - - public boolean isRead() { - return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); - } - - public void markRead() { - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (messages.get(i).isRead()) { - break; - } - this.messages.get(i).markRead(); - } - } - - public Message getLatestMarkableMessage() { - 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); - } - } - } - return null; - } - - public Message getLatestMessage() { - if (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 String getName() { - if (getMode() == MODE_MULTI) { - if (getMucOptions().getSubject() != null) { - return getMucOptions().getSubject(); - } else if (bookmark != null && bookmark.getName() != null) { - return bookmark.getName(); - } else { - String generatedName = getMucOptions().createNameFromParticipants(); - if (generatedName != null) { - return generatedName; - } else { - return getJid().getLocalpart(); - } - } - } else { - return this.getContact().getDisplayName(); - } - } - - 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(final Account account) { - this.account = account; - } - - @Override - public Jid getJid() { - 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.toString()); - 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) { - Jid jid; - try { - jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true); - } catch (final InvalidJidException e) { - // Borked DB.. - jid = null; - } - return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(NAME)), - cursor.getString(cursor.getColumnIndex(CONTACT)), - cursor.getString(cursor.getColumnIndex(ACCOUNT)), - jid, - 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(String presence, boolean sendStart) { - if (this.otrSession != null) { - return this.otrSession; - } else { - final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(), - presence, - "xmpp"); - this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine()); - try { - if (sendStart) { - this.otrSession.startSession(); - return this.otrSession; - } - return this.otrSession; - } catch (OtrException e) { - return null; - } - } - - } - - public SessionImpl getOtrSession() { - return this.otrSession; - } - - public void resetOtrSession() { - this.otrFingerprint = null; - this.otrSession = null; - this.mSmp.hint = null; - this.mSmp.secret = null; - this.mSmp.status = Smp.STATUS_NONE; - } - - public Smp smp() { - return mSmp; - } - - public 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 synchronized String getOtrFingerprint() { - if (this.otrFingerprint == null) { - try { - if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { - return null; - } - DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); - this.otrFingerprint = getAccount().getOtrEngine().getFingerprint(remotePubKey); - } catch (final OtrCryptoException | UnsupportedOperationException ignored) { - return null; - } - } - return this.otrFingerprint; - } - - public boolean verifyOtrFingerprint() { - final String fingerprint = getOtrFingerprint(); - if (fingerprint != null) { - getContact().addOtrFingerprint(fingerprint); - return true; - } else { - return false; - } - } - - public boolean isOtrFingerprintVerified() { - return getContact().getOtrFingerprints().contains(getOtrFingerprint()); - } - - 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(final Jid jid) { - this.contactJid = jid; - } - - public void setNextCounterpart(Jid jid) { - this.nextCounterpart = jid; - } - - public Jid getNextCounterpart() { - return this.nextCounterpart; - } - - 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 boolean smpRequested() { - return smp().status == Smp.STATUS_CONTACT_REQUESTED; - } - - 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) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).equals(message)) { - return true; - } - } - } - return false; - } - - public Message findSentMessageWithBody(String body) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) { - return message; - } - } - return null; - } - } - - public boolean setLastMessageTransmitted(long value) { - long before = getLastMessageTransmitted(); - if (value - before > 1000) { - this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value)); - return true; - } else { - return false; - } - } - - public long getLastMessageTransmitted() { - long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0); - if (timestamp == 0) { - synchronized (this.messages) { - for(int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if (message.getStatus() == Message.STATUS_RECEIVED) { - return message.getTimeSent(); - } - } - } - } - return timestamp; - } - - public void setMutedTill(long value) { - this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); - } - - public boolean isMuted() { - return System.currentTimeMillis() < 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); - } - } - - public void sort() { - synchronized (this.messages) { - Collections.sort(this.messages, new Comparator() { - @Override - public int compare(Message left, Message right) { - if (left.getTimeSent() < right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() > right.getTimeSent()) { - return 1; - } else { - return 0; - } - } - }); - for(Message message : this.messages) { - message.untie(); - } - } - } - - public int unreadCount() { - synchronized (this.messages) { - int count = 0; - for(int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).isRead()) { - return count; - } - ++count; - } - return count; - } - } - - public class Smp { - public static final int STATUS_NONE = 0; - public static final int STATUS_CONTACT_REQUESTED = 1; - public static final int STATUS_WE_REQUESTED = 2; - public static final int STATUS_FAILED = 3; - public static final int STATUS_VERIFIED = 4; - - public String secret = null; - public String hint = null; - public int status = 0; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Downloadable.java b/src/main/java/eu/siacs/conversations/entities/Downloadable.java deleted file mode 100644 index d25bf93a..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Downloadable.java +++ /dev/null @@ -1,28 +0,0 @@ -package eu.siacs.conversations.entities; - -public interface Downloadable { - - public final String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"}; - 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 static final int STATUS_UPLOADING = 0x207; - - public boolean start(); - - public int getStatus(); - - public long getFileSize(); - - public int getProgress(); - - public String getMimeType(); - - public void cancel(); -} diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java deleted file mode 100644 index 7c8f95d1..00000000 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ /dev/null @@ -1,172 +0,0 @@ -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.net.URLConnection; -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 String mime; - - 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 String getMimeType() { - String path = this.getAbsolutePath(); - try { - String mime = URLConnection.guessContentTypeFromName(path.replace("#","")); - if (mime != null) { - return mime; - } else if (mime == null && path.endsWith(".webp")) { - return "image/webp"; - } else { - return ""; - } - } catch (final StringIndexOutOfBoundsException e) { - return ""; - } - } - - 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/src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java b/src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java deleted file mode 100644 index 03fceceb..00000000 --- a/src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java +++ /dev/null @@ -1,39 +0,0 @@ -package eu.siacs.conversations.entities; - -public class DownloadablePlaceholder implements Downloadable { - - private int status; - - public DownloadablePlaceholder(int status) { - this.status = status; - } - @Override - public boolean start() { - return false; - } - - @Override - public int getStatus() { - return status; - } - - @Override - public long getFileSize() { - return 0; - } - - @Override - public int getProgress() { - return 0; - } - - @Override - public String getMimeType() { - return ""; - } - - @Override - public void cancel() { - - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java deleted file mode 100644 index efc1c2b9..00000000 --- a/src/main/java/eu/siacs/conversations/entities/ListItem.java +++ /dev/null @@ -1,33 +0,0 @@ -package eu.siacs.conversations.entities; - -import java.util.List; - -import eu.siacs.conversations.xmpp.jid.Jid; - -public interface ListItem extends Comparable { - public String getDisplayName(); - - public Jid getJid(); - - public List getTags(); - - public final class Tag { - private final String name; - private final int color; - - public Tag(final String name, final int color) { - this.name = name; - this.color = color; - } - - public int getColor() { - return this.color; - } - - public String getName() { - return this.name; - } - } - - public boolean match(final String needle); -} diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java deleted file mode 100644 index bcb1ca24..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ /dev/null @@ -1,587 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Arrays; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -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_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_FILE = 2; - public static final int TYPE_STATUS = 3; - public static final int TYPE_PRIVATE = 4; - - public static final String CONVERSATION = "conversationUuid"; - public static final String COUNTERPART = "counterpart"; - public static final String TRUE_COUNTERPART = "trueCounterpart"; - public static final String BODY = "body"; - public static final String TIME_SENT = "timeSent"; - public static final String ENCRYPTION = "encryption"; - public static final String STATUS = "status"; - public static final String TYPE = "type"; - public static final String REMOTE_MSG_ID = "remoteMsgId"; - public static final String SERVER_MSG_ID = "serverMsgId"; - public static final String RELATIVE_FILE_PATH = "relativeFilePath"; - public static final String ME_COMMAND = "/me "; - - - public boolean markable = false; - protected String conversationUuid; - protected Jid counterpart; - protected Jid trueCounterpart; - protected String body; - protected String encryptedBody; - protected long timeSent; - protected int encryption; - protected int status; - protected int type; - protected String relativeFilePath; - protected boolean read = true; - protected String remoteMsgId = null; - protected String serverMsgId = null; - protected Conversation conversation = null; - protected Downloadable downloadable = null; - private Message mNextMessage = null; - private Message mPreviousMessage = null; - - private Message() { - - } - - public Message(Conversation conversation, String body, int encryption) { - this(conversation, body, encryption, STATUS_UNSEND); - } - - public Message(Conversation conversation, String body, int encryption, int status) { - this(java.util.UUID.randomUUID().toString(), - conversation.getUuid(), - conversation.getJid() == null ? null : conversation.getJid().toBareJid(), - null, - body, - System.currentTimeMillis(), - encryption, - status, - TYPE_TEXT, - null, - null, - null); - this.conversation = conversation; - } - - private Message(final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final String remoteMsgId, - final String relativeFilePath, final String serverMsgId) { - 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; - this.relativeFilePath = relativeFilePath; - this.serverMsgId = serverMsgId; - } - - public static Message fromCursor(Cursor cursor) { - Jid jid; - try { - String value = cursor.getString(cursor.getColumnIndex(COUNTERPART)); - if (value != null) { - jid = Jid.fromString(value, true); - } else { - jid = null; - } - } catch (InvalidJidException e) { - jid = null; - } - Jid trueCounterpart; - try { - String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)); - if (value != null) { - trueCounterpart = Jid.fromString(value, true); - } else { - trueCounterpart = null; - } - } catch (InvalidJidException e) { - trueCounterpart = null; - } - return new Message(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(CONVERSATION)), - jid, - trueCounterpart, - 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)), - cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); - } - - public static Message createStatusMessage(Conversation conversation, String body) { - Message message = new Message(); - message.setType(Message.TYPE_STATUS); - message.setConversation(conversation); - message.setBody(body); - return message; - } - - @Override - public ContentValues getContentValues() { - ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(CONVERSATION, conversationUuid); - if (counterpart == null) { - values.putNull(COUNTERPART); - } else { - values.put(COUNTERPART, counterpart.toString()); - } - if (trueCounterpart == null) { - values.putNull(TRUE_COUNTERPART); - } else { - values.put(TRUE_COUNTERPART, trueCounterpart.toString()); - } - 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); - values.put(RELATIVE_FILE_PATH, relativeFilePath); - values.put(SERVER_MSG_ID,serverMsgId); - return values; - } - - public String getConversationUuid() { - return conversationUuid; - } - - public Conversation getConversation() { - return this.conversation; - } - - public void setConversation(Conversation conv) { - this.conversation = conv; - } - - public Jid getCounterpart() { - return counterpart; - } - - public void setCounterpart(final Jid counterpart) { - this.counterpart = 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 void setBody(String body) { - this.body = body; - } - - public long getTimeSent() { - return timeSent; - } - - public int getEncryption() { - return encryption; - } - - public void setEncryption(int encryption) { - this.encryption = encryption; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getRelativeFilePath() { - return this.relativeFilePath; - } - - public void setRelativeFilePath(String path) { - this.relativeFilePath = path; - } - - public String getRemoteMsgId() { - return this.remoteMsgId; - } - - public void setRemoteMsgId(String id) { - this.remoteMsgId = id; - } - - public String getServerMsgId() { - return this.serverMsgId; - } - - public void setServerMsgId(String id) { - this.serverMsgId = id; - } - - 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 String getEncryptedBody() { - return this.encryptedBody; - } - - public void setEncryptedBody(String body) { - this.encryptedBody = body; - } - - public int getType() { - return this.type; - } - - public void setType(int type) { - this.type = type; - } - - public void setTrueCounterpart(Jid trueCounterpart) { - this.trueCounterpart = trueCounterpart; - } - - public Downloadable getDownloadable() { - return this.downloadable; - } - - public void setDownloadable(Downloadable downloadable) { - this.downloadable = downloadable; - } - - public boolean equals(Message message) { - if (this.serverMsgId != null && message.getServerMsgId() != null) { - return this.serverMsgId.equals(message.getServerMsgId()); - } else if (this.body == null || this.counterpart == null) { - return false; - } else if (message.getRemoteMsgId() != null) { - return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) - && this.counterpart.equals(message.getCounterpart()) - && this.body.equals(message.getBody()); - } else { - return this.remoteMsgId == null - && this.counterpart.equals(message.getCounterpart()) - && this.body.equals(message.getBody()) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.PING_TIMEOUT * 500; - } - } - - public Message next() { - synchronized (this.conversation.messages) { - if (this.mNextMessage == null) { - int index = this.conversation.messages.indexOf(this); - if (index < 0 || index >= this.conversation.messages.size() - 1) { - this.mNextMessage = null; - } else { - this.mNextMessage = this.conversation.messages.get(index + 1); - } - } - return this.mNextMessage; - } - } - - public Message prev() { - synchronized (this.conversation.messages) { - if (this.mPreviousMessage == null) { - 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 mergeable(final Message message) { - return message != null && - (message.getType() == Message.TYPE_TEXT && - this.getDownloadable() == null && - message.getDownloadable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && - isStatusMergeable(this.getStatus(),message.getStatus()) && - this.getEncryption() == message.getEncryption() && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && - !GeoHelper.isGeoUri(message.getBody()) && - !GeoHelper.isGeoUri(this.body) && - !message.bodyContainsDownloadable() && - !this.bodyContainsDownloadable() && - !message.getBody().startsWith(ME_COMMAND) && - !this.getBody().startsWith(ME_COMMAND) - ); - } - - private static boolean isStatusMergeable(int a, int b) { - return a == b || ( - ( a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND) - || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED) - ); - } - - public String getMergedBody() { - final Message next = this.next(); - if (this.mergeable(next)) { - return getBody() + '\n' + next.getMergedBody(); - } - return getBody(); - } - - public boolean hasMeCommand() { - return getMergedBody().startsWith(ME_COMMAND); - } - - public int getMergedStatus() { - final Message next = this.next(); - if (this.mergeable(next)) { - return next.getStatus(); - } - return getStatus(); - } - - public long getMergedTimeSent() { - Message next = this.next(); - if (this.mergeable(next)) { - return next.getMergedTimeSent(); - } else { - return getTimeSent(); - } - } - - public boolean wasMergedIntoPrevious() { - Message prev = this.prev(); - return prev != null && prev.mergeable(this); - } - - public boolean trusted() { - Contact contact = this.getContact(); - return (status > STATUS_RECEIVED || (contact != null && contact.trusted())); - } - - public boolean bodyContainsDownloadable() { - /** - * there are a few cases where spaces result in an unwanted behavior, e.g. - * "http://upload.mitsu-freunde-bw.de/uploads/2015/03/i43b4bpr8.png /abc.png" - * or more than one image link in one message. - */ - if (body.contains(" ")) { - return false; - } - try { - URL url = new URL(body); - if (!url.getProtocol().equalsIgnoreCase("http") - && !url.getProtocol().equalsIgnoreCase("https")) { - return false; - } - - String sUrlPath = url.getPath(); - if (sUrlPath == null || sUrlPath.isEmpty()) { - return false; - } - - int iSlashIndex = sUrlPath.lastIndexOf('/') + 1; - - String sLastUrlPath = sUrlPath.substring(iSlashIndex).toLowerCase(); - - String[] extensionParts = sLastUrlPath.split("\\."); - if (extensionParts.length == 2 - && Arrays.asList(Downloadable.VALID_IMAGE_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_IMAGE_EXTENSIONS).contains( - extensionParts[extensionParts.length - 2])) { - return true; - } else { - return false; - } - } catch (MalformedURLException e) { - return false; - } - } - - public ImageParams getImageParams() { - ImageParams params = getLegacyImageParams(); - if (params != null) { - return params; - } - 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]; - try { - params.url = new URL(parts[0]); - } catch (MalformedURLException e1) { - params.url = null; - } - } - } 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.url = new URL(parts[0]); - } catch (MalformedURLException e1) { - params.url = null; - } - 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 ImageParams getLegacyImageParams() { - ImageParams params = new ImageParams(); - if (body == null) { - return params; - } - String parts[] = body.split(","); - if (parts.length == 3) { - try { - params.size = Long.parseLong(parts[0]); - } catch (NumberFormatException e) { - return null; - } - try { - params.width = Integer.parseInt(parts[1]); - } catch (NumberFormatException e) { - return null; - } - try { - params.height = Integer.parseInt(parts[2]); - } catch (NumberFormatException e) { - return null; - } - return params; - } else { - return null; - } - } - - public void untie() { - this.mNextMessage = null; - this.mPreviousMessage = null; - } - - public boolean isFileOrImage() { - return type == TYPE_FILE || type == TYPE_IMAGE; - } - - public class ImageParams { - public URL url; - public long size = 0; - public int width = 0; - public int height = 0; - public String origin; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java deleted file mode 100644 index addee8db..00000000 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ /dev/null @@ -1,530 +0,0 @@ -package eu.siacs.conversations.entities; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; - -import android.annotation.SuppressLint; - -@SuppressLint("DefaultLocale") -public class MucOptions { - - public enum Affiliation { - OWNER("owner", 4, R.string.owner), - ADMIN("admin", 3, R.string.admin), - MEMBER("member", 2, R.string.member), - OUTCAST("outcast", 0, R.string.outcast), - NONE("none", 1, R.string.no_affiliation); - - private Affiliation(String string, int rank, int resId) { - this.string = string; - this.resId = resId; - this.rank = rank; - } - - private String string; - private int resId; - private int rank; - - public int getResId() { - return resId; - } - - @Override - public String toString() { - return this.string; - } - - public boolean outranks(Affiliation affiliation) { - return rank > affiliation.rank; - } - - public boolean ranks(Affiliation affiliation) { - return rank >= affiliation.rank; - } - } - - public enum Role { - MODERATOR("moderator", R.string.moderator), - VISITOR("visitor", R.string.visitor), - PARTICIPANT("participant", R.string.participant), - NONE("none", R.string.no_role); - - private Role(String string, int resId) { - this.string = string; - this.resId = resId; - } - - private String string; - private int resId; - - public int getResId() { - return resId; - } - - @Override - public String toString() { - return this.string; - } - } - - public static final int ERROR_NO_ERROR = 0; - public static final int ERROR_NICK_IN_USE = 1; - public static final int ERROR_UNKNOWN = 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_ROOM_CONFIG_CHANGED = "104"; - public static final String STATUS_CODE_SELF_PRESENCE = "110"; - public static final String STATUS_CODE_BANNED = "301"; - public static final String STATUS_CODE_CHANGED_NICK = "303"; - public static final String STATUS_CODE_KICKED = "307"; - public static final String STATUS_CODE_LOST_MEMBERSHIP = "321"; - - private interface OnEventListener { - public void onSuccess(); - - public void onFailure(); - } - - public interface OnRenameListener extends OnEventListener { - - } - - public interface OnJoinListener extends OnEventListener { - - } - - public class User { - private Role role = Role.NONE; - private Affiliation affiliation = Affiliation.NONE; - private String name; - private Jid jid; - private long pgpKeyId = 0; - - public String getName() { - return name; - } - - public void setName(String user) { - this.name = user; - } - - public void setJid(Jid jid) { - this.jid = jid; - } - - public Jid getJid() { - return this.jid; - } - - public Role getRole() { - return this.role; - } - - public void setRole(String role) { - role = role.toLowerCase(); - switch (role) { - case "moderator": - this.role = Role.MODERATOR; - break; - case "participant": - this.role = Role.PARTICIPANT; - break; - case "visitor": - this.role = Role.VISITOR; - break; - default: - this.role = Role.NONE; - break; - } - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } else if (!(other instanceof User)) { - return false; - } else { - User o = (User) other; - return name != null && name.equals(o.name) - && jid != null && jid.equals(o.jid) - && affiliation == o.affiliation - && role == o.role; - } - } - - public Affiliation getAffiliation() { - return this.affiliation; - } - - public void setAffiliation(String affiliation) { - affiliation = affiliation.toLowerCase(); - switch (affiliation) { - case "admin": - this.affiliation = Affiliation.ADMIN; - break; - case "owner": - this.affiliation = Affiliation.OWNER; - break; - case "member": - this.affiliation = Affiliation.MEMBER; - break; - case "outcast": - this.affiliation = Affiliation.OUTCAST; - break; - default: - 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 List features = new ArrayList<>(); - private Conversation conversation; - private boolean isOnline = false; - private int error = ERROR_UNKNOWN; - private OnRenameListener onRenameListener = null; - private OnJoinListener onJoinListener = null; - private User self = new User(); - private String subject = null; - private String password = null; - private boolean mNickChangingInProgress = false; - - public MucOptions(Conversation conversation) { - this.account = conversation.getAccount(); - this.conversation = conversation; - } - - public void updateFeatures(ArrayList features) { - this.features.clear(); - this.features.addAll(features); - } - - public boolean hasFeature(String feature) { - return this.features.contains(feature); - } - - public boolean canInvite() { - return !membersOnly() || self.getAffiliation().ranks(Affiliation.ADMIN); - } - - public boolean membersOnly() { - return hasFeature("muc_membersonly"); - } - - public boolean nonanonymous() { - return hasFeature("muc_nonanonymous"); - } - - public boolean persistent() { - return hasFeature("muc_persistent"); - } - - 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) { - final Jid from = packet.getFrom(); - if (!from.isBareJid()) { - final String name = from.getResourcepart(); - final String type = packet.getAttribute("type"); - final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user"); - final List codes = getStatusCodes(x); - if (type == null) { - User user = new User(); - if (x != null) { - Element item = x.findChild("item"); - if (item != null && name != null) { - user.setName(name); - user.setAffiliation(item.getAttribute("affiliation")); - user.setRole(item.getAttribute("role")); - user.setJid(item.getAttributeAsJid("jid")); - if (codes.contains(STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(this.conversation.getJid())) { - this.isOnline = true; - this.error = ERROR_NO_ERROR; - self = user; - if (mNickChangingInProgress) { - onRenameListener.onSuccess(); - mNickChangingInProgress = false; - } else if (this.onJoinListener != null) { - this.onJoinListener.onSuccess(); - this.onJoinListener = null; - } - } else { - addUser(user); - } - if (pgp != null) { - Element signed = packet.findChild("x", "jabber:x:signed"); - if (signed != null) { - Element status = packet.findChild("status"); - String msg; - if (status != null) { - msg = status.getContent(); - } else { - msg = ""; - } - user.setPgpKeyId(pgp.fetchKeyId(account, msg, - signed.getContent())); - } - } - } - } - } else if (type.equals("unavailable")) { - if (codes.contains(STATUS_CODE_SELF_PRESENCE) || - packet.getFrom().equals(this.conversation.getJid())) { - if (codes.contains(STATUS_CODE_CHANGED_NICK)) { - this.mNickChangingInProgress = true; - } else if (codes.contains(STATUS_CODE_KICKED)) { - setError(KICKED_FROM_ROOM); - } else if (codes.contains(STATUS_CODE_BANNED)) { - setError(ERROR_BANNED); - } else if (codes.contains(STATUS_CODE_LOST_MEMBERSHIP)) { - setError(ERROR_MEMBERS_ONLY); - } else { - setError(ERROR_UNKNOWN); - } - } else { - deleteUser(name); - } - } else if (type.equals("error")) { - Element error = packet.findChild("error"); - if (error != null && error.hasChild("conflict")) { - if (isOnline) { - if (onRenameListener != null) { - onRenameListener.onFailure(); - } - } else { - setError(ERROR_NICK_IN_USE); - } - } else if (error != null && error.hasChild("not-authorized")) { - setError(ERROR_PASSWORD_REQUIRED); - } else if (error != null && error.hasChild("forbidden")) { - setError(ERROR_BANNED); - } else if (error != null && error.hasChild("registration-required")) { - setError(ERROR_MEMBERS_ONLY); - } else { - setError(ERROR_UNKNOWN); - } - } - } - } - - private void setError(int error) { - this.isOnline = false; - this.error = error; - if (onJoinListener != null) { - onJoinListener.onFailure(); - onJoinListener = null; - } - } - - private List getStatusCodes(Element x) { - List codes = new ArrayList<>(); - if (x != null) { - for (Element child : x.getChildren()) { - if (child.getName().equals("status")) { - String code = child.getAttribute("code"); - if (code != null) { - codes.add(code); - } - } - } - } - return codes; - } - - public List getUsers() { - return this.users; - } - - public String getProposedNick() { - if (conversation.getBookmark() != null - && conversation.getBookmark().getNick() != null - && !conversation.getBookmark().getNick().isEmpty()) { - return conversation.getBookmark().getNick(); - } else if (!conversation.getJid().isBareJid()) { - return conversation.getJid().getResourcepart(); - } else { - return account.getUsername(); - } - } - - public String getActualNick() { - if (this.self.getName() != null) { - return this.self.getName(); - } else { - return this.getProposedNick(); - } - } - - public boolean online() { - return this.isOnline; - } - - public int getError() { - return this.error; - } - - public void setOnRenameListener(OnRenameListener listener) { - this.onRenameListener = listener; - } - - public void setOnJoinListener(OnJoinListener listener) { - this.onJoinListener = listener; - } - - 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 String createNameFromParticipants() { - if (users.size() >= 2) { - List names = new ArrayList(); - for (User user : users) { - Contact contact = user.getContact(); - if (contact != null && !contact.getDisplayName().isEmpty()) { - names.add(contact.getDisplayName().split("\\s+")[0]); - } else { - names.add(user.getName()); - } - } - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < names.size(); ++i) { - builder.append(names.get(i)); - if (i != names.size() - 1) { - builder.append(", "); - } - } - return builder.toString(); - } else { - return null; - } - } - - 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 Jid createJoinJid(String nick) { - try { - return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick); - } catch (final InvalidJidException e) { - return null; - } - } - - public Jid 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; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java deleted file mode 100644 index bccf3117..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ /dev/null @@ -1,90 +0,0 @@ -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) { - synchronized (this.presences) { - this.presences.put(resource, status); - } - } - - public void removePresence(String resource) { - synchronized (this.presences) { - this.presences.remove(resource); - } - } - - public void clearPresences() { - synchronized (this.presences) { - this.presences.clear(); - } - } - - public int getMostAvailableStatus() { - int status = OFFLINE; - synchronized (this.presences) { - 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() { - synchronized (this.presences) { - return presences.size(); - } - } - - public String[] asStringArray() { - synchronized (this.presences) { - final String[] presencesArray = new String[presences.size()]; - presences.keySet().toArray(presencesArray); - return presencesArray; - } - } - - public boolean has(String presence) { - synchronized (this.presences) { - return presences.containsKey(presence); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java deleted file mode 100644 index ce058004..00000000 --- a/src/main/java/eu/siacs/conversations/entities/Roster.java +++ /dev/null @@ -1,91 +0,0 @@ -package eu.siacs.conversations.entities; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import eu.siacs.conversations.xmpp.jid.Jid; - -public class Roster { - final Account account; - final HashMap contacts = new HashMap<>(); - private String version = null; - - public Roster(Account account) { - this.account = account; - } - - public Contact getContactFromRoster(Jid jid) { - if (jid == null) { - return null; - } - synchronized (this.contacts) { - Contact contact = contacts.get(jid.toBareJid().toString()); - if (contact != null && contact.showInRoster()) { - return contact; - } else { - return null; - } - } - } - - public Contact getContact(final Jid jid) { - synchronized (this.contacts) { - final Jid bareJid = jid.toBareJid(); - if (contacts.containsKey(bareJid.toString())) { - return contacts.get(bareJid.toString()); - } else { - Contact contact = new Contact(bareJid); - contact.setAccount(account); - contacts.put(bareJid.toString(), 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() { - synchronized (this.contacts) { - return new ArrayList<>(this.contacts.values()); - } - } - - public void initContact(final Contact contact) { - contact.setAccount(account); - contact.setOption(Contact.Options.IN_ROSTER); - synchronized (this.contacts) { - contacts.put(contact.getJid().toBareJid().toString(), contact); - } - } - - public void setVersion(String version) { - this.version = version; - } - - public String getVersion() { - return this.version; - } - - public Account getAccount() { - return this.account; - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java deleted file mode 100644 index 186b4b98..00000000 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ /dev/null @@ -1,91 +0,0 @@ -package eu.siacs.conversations.generator; - -import android.util.Base64; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -import de.tzur.conversations.Settings; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.PhoneHelper; - -public abstract class AbstractGenerator { - private 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", - "http://jabber.org/protocol/muc", - "jabber:x:conference", - "http://jabber.org/protocol/caps", - "http://jabber.org/protocol/disco#info", - "urn:xmpp:avatar:metadata+notify", - "urn:xmpp:ping", - "jabber:iq:version", - "http://jabber.org/protocol/chatstates"}; - private final String[] MESSAGE_CONFIRMATION_FEATURES = { - "urn:xmpp:chat-markers:0", - "urn:xmpp:receipts" - }; - private String mVersion = null; - public final String IDENTITY_NAME = "Conversations"; - public final String IDENTITY_TYPE = "phone"; - - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - - protected XmppConnectionService mXmppConnectionService; - - protected AbstractGenerator(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - protected String getIdentityVersion() { - if (mVersion == null) { - this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); - } - return this.mVersion; - } - - protected String getIdentityName() { - return IDENTITY_NAME + " " + getIdentityVersion(); - } - - public String getCapHash() { - StringBuilder s = new StringBuilder(); - s.append("client/" + IDENTITY_TYPE + "//" + getIdentityName() + "<"); - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - - for (String feature : getFeatures()) { - s.append(feature + "<"); - } - byte[] sha1 = md.digest(s.toString().getBytes()); - return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); - } - - public static String getTimestamp(long time) { - DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - return DATE_FORMAT.format(time); - } - - public List getFeatures() { - ArrayList features = new ArrayList<>(); - features.addAll(Arrays.asList(FEATURES)); - if (Settings.CONFIRM_MESSAGE_RECEIVED) { - features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); - } - Collections.sort(features); - return features; - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java deleted file mode 100644 index 6bc629b5..00000000 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ /dev/null @@ -1,190 +0,0 @@ -package eu.siacs.conversations.generator; - - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.utils.Xmlns; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.jid.Jid; -import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public class IqGenerator extends AbstractGenerator { - - public IqGenerator(final XmppConnectionService service) { - super(service); - } - - public IqPacket discoResponse(final IqPacket request) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); - packet.setId(request.getId()); - packet.setTo(request.getFrom()); - final Element query = packet.addChild("query", - "http://jabber.org/protocol/disco#info"); - query.setAttribute("node", request.query().getAttribute("node")); - final Element identity = query.addChild("identity"); - identity.setAttribute("category", "client"); - identity.setAttribute("type", IDENTITY_TYPE); - identity.setAttribute("name", getIdentityName()); - for (final String feature : getFeatures()) { - query.addChild("feature").setAttribute("var", feature); - } - return packet; - } - - public IqPacket versionResponse(final IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); - Element query = packet.query("jabber:iq:version"); - query.addChild("name").setContent(IDENTITY_NAME); - query.addChild("version").setContent(getIdentityVersion()); - return packet; - } - - protected IqPacket publish(final String node, final Element item) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element pubsub = packet.addChild("pubsub", - "http://jabber.org/protocol/pubsub"); - final Element publish = pubsub.addChild("publish"); - publish.setAttribute("node", node); - publish.addChild(item); - return packet; - } - - protected IqPacket retrieve(String node, Element item) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - final Element pubsub = packet.addChild("pubsub", - "http://jabber.org/protocol/pubsub"); - final Element items = pubsub.addChild("items"); - items.setAttribute("node", node); - if (item != null) { - items.addChild(item); - } - return packet; - } - - public IqPacket publishAvatar(Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element data = item.addChild("data", "urn:xmpp:avatar:data"); - data.setContent(avatar.image); - return publish("urn:xmpp:avatar:data", item); - } - - public IqPacket publishAvatarMetadata(final Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element metadata = item - .addChild("metadata", "urn:xmpp:avatar:metadata"); - final 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(final Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); - packet.setTo(avatar.owner); - return packet; - } - - public IqPacket retrieveAvatarMetaData(final Jid to) { - final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); - if (to != null) { - packet.setTo(to); - } - return packet; - } - - public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element query = packet.query("urn:xmpp:mam:0"); - query.setAttribute("queryid",mam.getQueryId()); - final Data data = new Data(); - data.setFormType("urn:xmpp:mam:0"); - if (mam.muc()) { - packet.setTo(mam.getWith()); - } else if (mam.getWith()!=null) { - data.put("with", mam.getWith().toString()); - } - data.put("start",getTimestamp(mam.getStart())); - data.put("end",getTimestamp(mam.getEnd())); - query.addChild(data); - if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { - query.addChild("set", "http://jabber.org/protocol/rsm").addChild("before").setContent(mam.getReference()); - } else if (mam.getReference() != null) { - query.addChild("set", "http://jabber.org/protocol/rsm").addChild("after").setContent(mam.getReference()); - } - return packet; - } - public IqPacket generateGetBlockList() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.addChild("blocklist", Xmlns.BLOCKING); - - return iq; - } - - public IqPacket generateSetBlockRequest(final Jid jid) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final Element block = iq.addChild("block", Xmlns.BLOCKING); - block.addChild("item").setAttribute("jid", jid.toBareJid().toString()); - return iq; - } - - public IqPacket generateSetUnblockRequest(final Jid jid) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final Element block = iq.addChild("unblock", Xmlns.BLOCKING); - block.addChild("item").setAttribute("jid", jid.toBareJid().toString()); - return iq; - } - - public IqPacket generateSetPassword(final Account account, final String newPassword) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(account.getServer()); - final Element query = packet.addChild("query", Xmlns.REGISTER); - final Jid jid = account.getJid(); - query.addChild("username").setContent(jid.getLocalpart()); - query.addChild("password").setContent(newPassword); - return packet; - } - - public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { - List jids = new ArrayList<>(); - jids.add(jid); - return changeAffiliation(conference,jids,affiliation); - } - - public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(conference.getJid().toBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - for(Jid jid : jids) { - Element item = query.addChild("item"); - item.setAttribute("jid", jid.toString()); - item.setAttribute("affiliation", affiliation); - } - return packet; - } - - public IqPacket changeRole(Conversation conference, String nick, String role) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(conference.getJid().toBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); - item.setAttribute("nick", nick); - item.setAttribute("role", role); - return packet; - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java deleted file mode 100644 index 8f6a90b9..00000000 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ /dev/null @@ -1,187 +0,0 @@ -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.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jid.Jid; -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); - if (this.mXmppConnectionService.indicateReceived()) { - packet.addChild("request", "urn:xmpp:receipts"); - } - } else { - packet.setTo(message.getCounterpart().toBareJid()); - packet.setType(MessagePacket.TYPE_GROUPCHAT); - } - packet.setFrom(account.getJid()); - 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())[0]); - 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 generateChatState(Conversation conversation) { - final Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); - packet.setTo(conversation.getJid().toBareJid()); - packet.setFrom(account.getJid()); - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - return packet; - } - - public MessagePacket confirm(final Account account, final Jid to, final String id) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); - packet.setTo(to); - packet.setFrom(account.getJid()); - 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.getJid().toBareJid()); - Element subjectChild = new Element("subject"); - subjectChild.setContent(subject); - packet.addChild(subjectChild); - packet.setFrom(conversation.getAccount().getJid().toBareJid()); - return packet; - } - - public MessagePacket directInvite(final Conversation conversation, final Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); - packet.setTo(contact); - packet.setFrom(conversation.getAccount().getJid()); - Element x = packet.addChild("x", "jabber:x:conference"); - x.setAttribute("jid", conversation.getJid().toBareJid().toString()); - return packet; - } - - public MessagePacket invite(Conversation conversation, Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setTo(conversation.getJid().toBareJid()); - packet.setFrom(conversation.getAccount().getJid()); - Element x = new Element("x"); - x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); - Element invite = new Element("invite"); - invite.setAttribute("to", contact.toBareJid().toString()); - 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.getJid()); - Element received = receivedPacket.addChild("received", namespace); - received.setAttribute("id", originalMessage.getId()); - return receivedPacket; - } - - public MessagePacket generateOtrError(Jid to, String id) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_ERROR); - packet.setAttribute("id",id); - packet.setTo(to); - Element error = packet.addChild("error"); - error.setAttribute("code","406"); - error.setAttribute("type","modify"); - error.addChild("not-acceptable","urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild("text").setContent("unreadable OTR message received"); - return packet; - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java deleted file mode 100644 index 1e896724..00000000 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ /dev/null @@ -1,57 +0,0 @@ -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.setTo(contact.getJid()); - packet.setFrom(contact.getAccount().getJid().toBareJid()); - 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.setFrom(account.getJid()); - 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.im"); - cap.setAttribute("ver", capHash); - } - return packet; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/src/main/java/eu/siacs/conversations/http/HttpConnection.java deleted file mode 100644 index e7d30919..00000000 --- a/src/main/java/eu/siacs/conversations/http/HttpConnection.java +++ /dev/null @@ -1,310 +0,0 @@ -package eu.siacs.conversations.http; - -import android.content.Intent; -import android.net.Uri; -import android.os.SystemClock; - -import org.apache.http.conn.ssl.StrictHostnameVerifier; - -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 java.util.Arrays; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -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; - private boolean acceptedAutomatically = false; - private int mProgress = 0; - private long mLastGuiRefresh = 0; - - 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()); - String[] parts = mUrl.getPath().toLowerCase().split("\\."); - String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null; - String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null; - if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) { - this.message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR) { - this.message.setEncryption(Message.ENCRYPTION_NONE); - } - String extension; - if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) { - extension = secondToLast; - } else { - extension = lastPart; - } - message.setRelativeFilePath(message.getUuid()+"."+extension); - 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); - mXmppConnectionService.updateConversationUi(); - if (acceptedAutomatically) { - mXmppConnectionService.getNotificationService().push(message); - } - } - - private void changeStatus(int status) { - this.mStatus = status; - mXmppConnectionService.updateConversationUi(); - } - - private void setupTrustManager(final HttpsURLConnection connection, - final boolean interactive) { - final X509TrustManager trustManager; - final 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 { - final SSLContext sc = SSLContext.getInstance("TLS"); - sc.init(null, new X509TrustManager[]{trustManager}, - mXmppConnectionService.getRNG()); - - final SSLSocketFactory sf = sc.getSocketFactory(); - final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( - sf.getSupportedCipherSuites()); - if (cipherSuites.length > 0) { - sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); - - } - - connection.setSSLSocketFactory(sf); - connection.setHostnameVerifier(hostnameVerifier); - } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { - } - } - - 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); - HttpConnection.this.acceptedAutomatically = false; - HttpConnection.this.mXmppConnectionService.getNotificationService().push(message); - return; - } catch (IOException e) { - cancel(); - return; - } - file.setExpectedSize(size); - if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) { - HttpConnection.this.acceptedAutomatically = true; - new Thread(new FileDownloader(interactive)).start(); - } else { - changeStatus(STATUS_OFFER); - HttpConnection.this.acceptedAutomatically = false; - HttpConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - } - - 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()); - file.getParentFile().mkdirs(); - file.createNewFile(); - OutputStream os = file.createOutputStream(); - if (os == null) { - throw new IOException(); - } - long transmitted = 0; - long expected = file.getExpectedSize(); - int count = -1; - byte[] buffer = new byte[1024]; - while ((count = is.read(buffer)) != -1) { - transmitted += count; - os.write(buffer, 0, count); - updateProgress((int) ((((double) transmitted) / expected) * 100)); - } - os.flush(); - os.close(); - is.close(); - } - - private void updateImageBounds() { - message.setType(Message.TYPE_IMAGE); - mXmppConnectionService.getFileBackend().updateFileParams(message,mUrl); - mXmppConnectionService.updateMessage(message); - } - - } - - public void updateProgress(int i) { - this.mProgress = i; - if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { - this.mLastGuiRefresh = SystemClock.elapsedRealtime(); - mXmppConnectionService.updateConversationUi(); - } - } - - @Override - public int getStatus() { - return this.mStatus; - } - - @Override - public long getFileSize() { - if (this.file != null) { - return this.file.getExpectedSize(); - } else { - return 0; - } - } - - @Override - public int getProgress() { - return this.mProgress; - } - - @Override - public String getMimeType() { - return ""; - } -} diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java deleted file mode 100644 index 9a2a2405..00000000 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ /dev/null @@ -1,28 +0,0 @@ -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/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java deleted file mode 100644 index 473195bd..00000000 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ /dev/null @@ -1,109 +0,0 @@ -package eu.siacs.conversations.parser; - -import android.util.Log; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeFactory; - -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.jid.Jid; - -public abstract class AbstractParser { - - protected XmppConnectionService mXmppConnectionService; - - protected AbstractParser(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - /** - * Gets the timestamp from the 'delay' element. - * Refer to XEP-0203: Delayed Delivery for details. @link{http://xmpp.org/extensions/xep-0203.html} - * @param packet the element to find the child element 'delay' in. - * @return the time in milli seconds of the attribute 'stamp' of the - * element 'delay'. In case there is no 'delay' element or no 'stamp' - * attribute or the current time is less than the value of the 'stamp' - * attribute the current time is returned. - */ - protected long getTimestamp(Element packet) { - long now = System.currentTimeMillis(); - Element delay = packet.findChild("delay"); - if (delay == null) { - return now; - } - String stamp = delay.getAttribute("stamp"); - if (stamp == null) { - return now; - } - /*long time = parseTimestamp(stamp).getTime(); - return now < time ? now : time;*/ - try { - long time = parseTimestamp(stamp).getTime(); - return now < time ? now : time; - } catch (ParseException e) { - return now; - } - } - - /** - * Parses the timestamp according to XEP-0082: XMPP Date and Time Profiles. - * @link{http://xmpp.org/extensions/xep-0082.html} - * - * @param timestamp the timestamp to parse - * @return Date - * @throws ParseException - */ - public static Date parseTimestamp(String timestamp) throws ParseException { - /*try { - Log.d("TIMESTAMP", timestamp); - return DatatypeFactory.newInstance().newXMLGregorianCalendar(timestamp).toGregorianCalendar().getTime(); - } catch (DatatypeConfigurationException e) { - Log.d("TIMESTAMP", e.getMessage()); - return new Date(); - }*/ - timestamp = timestamp.replace("Z", "+0000"); - SimpleDateFormat dateFormat; - timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length()); - dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US); - return dateFormat.parse(timestamp); - } - - protected void updateLastseen(final Element packet, final Account account, - final boolean presenceOverwrite) { - final Jid from = packet.getAttributeAsJid("from"); - updateLastseen(packet, account, from, presenceOverwrite); - } - - protected void updateLastseen(final Element packet, final Account account, final Jid from, - final boolean presenceOverwrite) { - final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart(); - final Contact contact = account.getRoster().getContact(from); - final long timestamp = getTimestamp(packet); - if (timestamp >= contact.lastseen.time) { - contact.lastseen.time = timestamp; - if (!presence.isEmpty() && 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/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java deleted file mode 100644 index 6039d395..00000000 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ /dev/null @@ -1,158 +0,0 @@ -package eu.siacs.conversations.parser; - -import android.util.Log; - -import java.util.ArrayList; -import java.util.Collection; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.Xmlns; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.jid.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public class IqParser extends AbstractParser implements OnIqPacketReceived { - - public IqParser(final XmppConnectionService service) { - super(service); - } - - private void rosterItems(final Account account, final Element query) { - final String version = query.getAttribute("ver"); - if (version != null) { - account.getRoster().setVersion(version); - } - for (final Element item : query.getChildren()) { - if (item.getName().equals("item")) { - final Jid jid = item.getAttributeAsJid("jid"); - if (jid == null) { - continue; - } - final String name = item.getAttribute("name"); - final String subscription = item.getAttribute("subscription"); - final Contact contact = account.getRoster().getContact(jid); - if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { - contact.setServerName(name); - contact.parseGroupsFromElement(item); - } - 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.getAvatarService().clear(contact); - } - } - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - } - - public String avatarData(final IqPacket packet) { - final Element pubsub = packet.findChild("pubsub", - "http://jabber.org/protocol/pubsub"); - if (pubsub == null) { - return null; - } - final Element items = pubsub.findChild("items"); - if (items == null) { - return null; - } - return super.avatarData(items); - } - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) { - final Element query = packet.findChild("query"); - // If this is in response to a query for the whole roster: - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getRoster().markAllAsNotInRoster(); - } - this.rosterItems(account, query); - } else if ((packet.hasChild("block", Xmlns.BLOCKING) || packet.hasChild("blocklist", Xmlns.BLOCKING)) && - packet.fromServer(account)) { - // Block list or block push. - Log.d(Config.LOGTAG, "Received blocklist update from server"); - final Element blocklist = packet.findChild("blocklist", Xmlns.BLOCKING); - final Element block = packet.findChild("block", Xmlns.BLOCKING); - final Collection items = blocklist != null ? blocklist.getChildren() : - (block != null ? block.getChildren() : null); - // If this is a response to a blocklist query, clear the block list and replace with the new one. - // Otherwise, just update the existing blocklist. - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.clearBlocklist(); - account.getXmppConnection().getFeatures().setBlockListRequested(true); - } - if (items != null) { - final Collection jids = new ArrayList<>(items.size()); - // Create a collection of Jids from the packet - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = item.getAttributeAsJid("jid"); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().addAll(jids); - } - // Update the UI - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - } else if (packet.hasChild("unblock", Xmlns.BLOCKING) && - packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { - Log.d(Config.LOGTAG, "Received unblock update from server"); - final Collection items = packet.findChild("unblock", Xmlns.BLOCKING).getChildren(); - if (items.size() == 0) { - // No children to unblock == unblock all - account.getBlocklist().clear(); - } else { - final Collection jids = new ArrayList<>(items.size()); - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = item.getAttributeAsJid("jid"); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().removeAll(jids); - } - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - } 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")) { - final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("query","jabber:iq:version")) { - final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); - mXmppConnectionService.sendIqPacket(account,response,null); - } else if (packet.hasChild("ping", "urn:xmpp:ping")) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } else { - if ((packet.getType() == IqPacket.TYPE.GET) - || (packet.getType() == IqPacket.TYPE.SET)) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - final 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/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java deleted file mode 100644 index 9f36f1e9..00000000 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ /dev/null @@ -1,653 +0,0 @@ -package eu.siacs.conversations.parser; - -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionStatus; - -import de.tzur.conversations.Settings; -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.http.HttpConnectionManager; -import eu.siacs.conversations.services.MessageArchiveService; -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.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jid.Jid; -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 boolean extractChatState(Conversation conversation, final Element element) { - ChatState state = ChatState.parse(element); - if (state != null && conversation != null) { - final Account account = conversation.getAccount(); - Jid from = element.getAttributeAsJid("from"); - if (from != null && from.toBareJid().equals(account.getJid().toBareJid())) { - conversation.setOutgoingChatState(state); - return false; - } else { - return conversation.setIncomingChatState(state); - } - } - return false; - } - - private Message parseChat(MessagePacket packet, Account account) { - final Jid jid = packet.getFrom(); - if (jid == null) { - return null; - } - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid.toBareJid(), false); - String pgpBody = getPgpBody(packet); - Message finishedMessage; - if (pgpBody != null) { - finishedMessage = new Message(conversation, - pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED); - } else { - finishedMessage = new Message(conversation, - packet.getBody(), Message.ENCRYPTION_NONE, - Message.STATUS_RECEIVED); - } - finishedMessage.setRemoteMsgId(packet.getId()); - finishedMessage.markable = isMarkable(packet); - if (conversation.getMode() == Conversation.MODE_MULTI - && !jid.isBareJid()) { - final Jid trueCounterpart = conversation.getMucOptions() - .getTrueCounterpart(jid.getResourcepart()); - if (trueCounterpart != null) { - updateLastseen(packet, account, trueCounterpart, false); - } - finishedMessage.setType(Message.TYPE_PRIVATE); - finishedMessage.setTrueCounterpart(trueCounterpart); - if (conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - } else { - updateLastseen(packet, account, true); - } - finishedMessage.setCounterpart(jid); - finishedMessage.setTime(getTimestamp(packet)); - extractChatState(conversation,packet); - return finishedMessage; - } - - private Message parseOtrChat(MessagePacket packet, Account account) { - final Jid to = packet.getTo(); - final Jid from = packet.getFrom(); - if (to == null || from == null) { - return null; - } - boolean properlyAddressed = !to.isBareJid() || account.countPresences() == 1; - Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, from.toBareJid(), false); - String presence; - if (from.isBareJid()) { - presence = ""; - } else { - presence = from.getResourcepart(); - } - extractChatState(conversation, packet); - updateLastseen(packet, account, true); - String body = packet.getBody(); - if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { - conversation.endOtrIfNeeded(); - } - if (!conversation.hasValidOtrSession()) { - if (properlyAddressed) { - conversation.startOtrSession(presence,false); - } else { - return null; - } - } else { - String foreignPresence = conversation.getOtrSession() - .getSessionID().getUserID(); - if (!foreignPresence.equals(presence)) { - conversation.endOtrIfNeeded(); - if (properlyAddressed) { - conversation.startOtrSession(presence, false); - } else { - return null; - } - } - } - try { - conversation.setLastReceivedOtrMessageId(packet.getId()); - Session otrSession = conversation.getOtrSession(); - SessionStatus before = otrSession.getSessionStatus(); - body = otrSession.transformReceiving(body); - SessionStatus after = otrSession.getSessionStatus(); - if ((before != after) && (after == SessionStatus.ENCRYPTED)) { - conversation.setNextEncryption(Message.ENCRYPTION_OTR); - mXmppConnectionService.onOtrSessionEstablished(conversation); - } else if ((before != after) && (after == SessionStatus.FINISHED)) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - 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, body, Message.ENCRYPTION_OTR, - Message.STATUS_RECEIVED); - finishedMessage.setTime(getTimestamp(packet)); - finishedMessage.setRemoteMsgId(packet.getId()); - finishedMessage.markable = isMarkable(packet); - finishedMessage.setCounterpart(from); - conversation.setLastReceivedOtrMessageId(null); - return finishedMessage; - } catch (Exception e) { - conversation.resetOtrSession(); - return null; - } - } - - private Message parseGroupchat(MessagePacket packet, Account account) { - int status; - final Jid from = packet.getFrom(); - if (from == null) { - return null; - } - if (mXmppConnectionService.find(account.pendingConferenceLeaves, - account, from.toBareJid()) != null) { - return null; - } - Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, from.toBareJid(), true); - final Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(from.getResourcepart()); - if (trueCounterpart != null) { - updateLastseen(packet, account, trueCounterpart, false); - } - if (packet.hasChild("subject")) { - conversation.setHasMessagesLeftOnServer(true); - conversation.getMucOptions().setSubject(packet.findChild("subject").getContent()); - mXmppConnectionService.updateConversationUi(); - return null; - } - - final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user"); - if (from.isBareJid() && (x == null || !x.hasChild("status"))) { - return null; - } else if (from.isBareJid() && x.hasChild("status")) { - for(Element child : x.getChildren()) { - if (child.getName().equals("status")) { - String code = child.getAttribute("code"); - if (code.contains(MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED)) { - mXmppConnectionService.fetchConferenceConfiguration(conversation); - } - } - } - return null; - } - - if (from.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { - if (mXmppConnectionService.markMessage(conversation, - packet.getId(), Message.STATUS_SEND_RECEIVED)) { - return null; - } else if (packet.getId() == null) { - Message message = conversation.findSentMessageWithBody(packet.getBody()); - if (message != null) { - mXmppConnectionService.markMessage(message,Message.STATUS_SEND_RECEIVED); - return null; - } else { - status = Message.STATUS_SEND; - } - } else { - status = Message.STATUS_SEND; - } - } else { - status = Message.STATUS_RECEIVED; - } - String pgpBody = getPgpBody(packet); - Message finishedMessage; - if (pgpBody == null) { - finishedMessage = new Message(conversation, - packet.getBody(), Message.ENCRYPTION_NONE, status); - } else { - finishedMessage = new Message(conversation, pgpBody, - Message.ENCRYPTION_PGP, status); - } - finishedMessage.setRemoteMsgId(packet.getId()); - finishedMessage.markable = isMarkable(packet); - finishedMessage.setCounterpart(from); - if (status == Message.STATUS_RECEIVED) { - finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(from.getResourcepart())); - } - if (packet.hasChild("delay") - && conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - finishedMessage.setTime(getTimestamp(packet)); - return finishedMessage; - } - - private Message parseCarbonMessage(final MessagePacket packet, final Account account) { - int status; - final Jid 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")) { - final Jid to = message.getAttributeAsJid("to"); - if (to != null) { - final Conversation conversation = mXmppConnectionService.find( - mXmppConnectionService.getConversations(), account, - to.toBareJid()); - if (conversation != null) { - mXmppConnectionService.markRead(conversation); - } - } - } - return null; - } - if (status == Message.STATUS_RECEIVED) { - fullJid = message.getAttributeAsJid("from"); - if (fullJid == null) { - return null; - } else { - updateLastseen(message, account, true); - } - } else { - fullJid = message.getAttributeAsJid("to"); - if (fullJid == null) { - return null; - } - } - if (message.hasChild("x","http://jabber.org/protocol/muc#user") - && "chat".equals(message.getAttribute("type"))) { - return null; - } - Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, fullJid.toBareJid(), false); - String pgpBody = getPgpBody(message); - Message finishedMessage; - if (pgpBody != null) { - finishedMessage = new Message(conversation, pgpBody, - Message.ENCRYPTION_PGP, status); - } else { - String body = message.findChild("body").getContent(); - finishedMessage = new Message(conversation, body, - Message.ENCRYPTION_NONE, status); - } - extractChatState(conversation,message); - finishedMessage.setTime(getTimestamp(message)); - finishedMessage.setRemoteMsgId(message.getAttribute("id")); - finishedMessage.markable = isMarkable(message); - finishedMessage.setCounterpart(fullJid); - if (conversation.getMode() == Conversation.MODE_MULTI - && !fullJid.isBareJid()) { - finishedMessage.setType(Message.TYPE_PRIVATE); - finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(fullJid.getResourcepart())); - if (conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - } - return finishedMessage; - } - - private Message parseMamMessage(MessagePacket packet, final Account account) { - final Element result = packet.findChild("result","urn:xmpp:mam:0"); - if (result == null ) { - return null; - } - final MessageArchiveService.Query query = this.mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid")); - if (query!=null) { - query.incrementTotalCount(); - } - final Element forwarded = result.findChild("forwarded","urn:xmpp:forward:0"); - if (forwarded == null) { - return null; - } - final Element message = forwarded.findChild("message"); - if (message == null) { - return null; - } - final Element body = message.findChild("body"); - if (body == null || message.hasChild("private","urn:xmpp:carbons:2") || message.hasChild("no-copy","urn:xmpp:hints")) { - return null; - } - int encryption; - String content = getPgpBody(message); - if (content != null) { - encryption = Message.ENCRYPTION_PGP; - } else { - encryption = Message.ENCRYPTION_NONE; - content = body.getContent(); - } - if (content == null) { - return null; - } - final long timestamp = getTimestamp(forwarded); - final Jid to = message.getAttributeAsJid("to"); - final Jid from = message.getAttributeAsJid("from"); - Jid counterpart; - int status; - Conversation conversation; - if (from!=null && to != null && from.toBareJid().equals(account.getJid().toBareJid())) { - status = Message.STATUS_SEND; - conversation = this.mXmppConnectionService.findOrCreateConversation(account,to.toBareJid(),false,query); - counterpart = to; - } else if (from !=null && to != null) { - status = Message.STATUS_RECEIVED; - conversation = this.mXmppConnectionService.findOrCreateConversation(account,from.toBareJid(),false,query); - counterpart = from; - } else { - return null; - } - Message finishedMessage = new Message(conversation,content,encryption,status); - finishedMessage.setTime(timestamp); - finishedMessage.setCounterpart(counterpart); - finishedMessage.setRemoteMsgId(message.getAttribute("id")); - finishedMessage.setServerMsgId(result.getAttribute("id")); - if (conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - if (query!=null) { - query.incrementMessageCount(); - } - return finishedMessage; - } - - private void parseError(final MessagePacket packet, final Account account) { - final Jid from = packet.getFrom(); - mXmppConnectionService.markMessage(account, from.toBareJid(), - packet.getId(), Message.STATUS_SEND_FAILED); - } - - private void parseNonMessage(Element packet, Account account) { - final Jid from = packet.getAttributeAsJid("from"); - if (extractChatState(from == null ? null : mXmppConnectionService.find(account,from), packet)) { - mXmppConnectionService.updateConversationUi(); - } - Element invite = extractInvite(packet); - if (invite != null) { - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, from, true); - if (!conversation.getMucOptions().online()) { - Element password = invite.findChild("password"); - conversation.getMucOptions().setPassword(password == null ? null : password.getContent()); - mXmppConnectionService.databaseBackend.updateConversation(conversation); - mXmppConnectionService.joinMuc(conversation); - mXmppConnectionService.updateConversationUi(); - } - } - if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { - Element event = packet.findChild("event", - "http://jabber.org/protocol/pubsub#event"); - parseEvent(event, 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); - final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), id, Message.STATUS_SEND_DISPLAYED); - Message message = displayedMessage == null ? null :displayedMessage.prev(); - while (message != null - && message.getStatus() == Message.STATUS_SEND_RECEIVED - && message.getTimeSent() < displayedMessage.getTimeSent()) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED); - message = message.prev(); - } - } 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.toBareJid(), - 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.toBareJid(), - id, Message.STATUS_SEND_RECEIVED); - } - } - - private Element extractInvite(Element message) { - Element x = message.findChild("x","http://jabber.org/protocol/muc#user"); - if (x == null) { - x = message.findChild("x","jabber:x:conference"); - } - if (x != null && x.hasChild("invite")) { - return x; - } else { - return null; - } - } - - private void parseEvent(final Element event, final Jid from, final Account account) { - Element items = event.findChild("items"); - if (items == null) { - return; - } - String node = items.getAttribute("node"); - if (node == null) { - return; - } - 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().toBareJid().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()); - mXmppConnectionService.getAvatarService().clear(account); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } - } - } - } - } - - 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; - 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") && extractInvite(packet) == null) { - 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(); - mXmppConnectionService.markRead(message.getConversation()); - } else { - message.markUnread(); - } - } - } else if (packet.hasChild("result","urn:xmpp:mam:0")) { - message = parseMamMessage(packet, account); - if (message != null) { - Conversation conversation = message.getConversation(); - conversation.add(message); - mXmppConnectionService.databaseBackend.createMessage(message); - } - return; - } else if (packet.hasChild("fin","urn:xmpp:mam:0")) { - Element fin = packet.findChild("fin","urn:xmpp:mam:0"); - mXmppConnectionService.getMessageArchiveService().processFin(fin); - } 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(); - } else { - mXmppConnectionService.markRead(message.getConversation()); - account.activateGracePeriod(); - } - } - } 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 ((Settings.CONFIRM_MESSAGE_RECEIVED) - && ((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 (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().advancedStreamFeaturesLoaded()) { - if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { - mXmppConnectionService.updateConversation(conversation); - } - } - - if (message.getStatus() == Message.STATUS_RECEIVED - && conversation.getOtrSession() != null - && !conversation.getOtrSession().getSessionID().getUserID() - .equals(message.getCounterpart().getResourcepart())) { - conversation.endOtrIfNeeded(); - } - - if (packet.getType() != MessagePacket.TYPE_ERROR) { - if (message.getEncryption() == Message.ENCRYPTION_NONE - || mXmppConnectionService.saveEncryptedMessages()) { - mXmppConnectionService.databaseBackend.createMessage(message); - } - } - final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); - if (message.trusted() - && Settings.DOWNLOAD_IMAGE_LINKS - && mXmppConnectionService.isDownloadAllowedInConnection() - && message.bodyContainsDownloadable() - && manager.getAutoAcceptFileSize() > 0) { - manager.createNewConnection(message); - } else if (!message.isRead()) { - 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/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java deleted file mode 100644 index 7505b091..00000000 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ /dev/null @@ -1,118 +0,0 @@ -package eu.siacs.conversations.parser; - -import java.util.ArrayList; - -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.MucOptions; -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.jid.Jid; -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(); - final Conversation conversation = packet.getFrom() == null ? null : mXmppConnectionService.find(account, packet.getFrom().toBareJid()); - if (conversation != null) { - final MucOptions mucOptions = conversation.getMucOptions(); - boolean before = mucOptions.online(); - int count = mucOptions.getUsers().size(); - final ArrayList tileUserBefore = new ArrayList<>(mucOptions.getUsers().subList(0,Math.min(mucOptions.getUsers().size(),5))); - mucOptions.processPacket(packet, mPgpEngine); - final ArrayList tileUserAfter = new ArrayList<>(mucOptions.getUsers().subList(0,Math.min(mucOptions.getUsers().size(),5))); - if (!tileUserAfter.equals(tileUserBefore)) { - mXmppConnectionService.getAvatarService().clear(conversation); - } - if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUsers().size())) { - mXmppConnectionService.updateConversationUi(); - } else if (mucOptions.online()) { - mXmppConnectionService.updateMucRosterUi(); - } - } - } - - public void parseContactPresence(PresencePacket packet, Account account) { - PresenceGenerator mPresenceGenerator = mXmppConnectionService - .getPresenceGenerator(); - if (packet.getFrom() == null) { - return; - } - final Jid from = packet.getFrom(); - String type = packet.getAttribute("type"); - Contact contact = account.getRoster().getContact(packet.getFrom()); - if (type == null) { - String presence; - if (!from.isBareJid()) { - presence = from.getResourcepart(); - } 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, false); - mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online); - } else if (type.equals("unavailable")) { - if (from.isBareJid()) { - contact.clearPresences(); - } else { - contact.removePresence(from.getResourcepart()); - } - 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/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java deleted file mode 100644 index 28e1c47e..00000000 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ /dev/null @@ -1,400 +0,0 @@ -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 eu.siacs.conversations.xmpp.jid.Jid; - -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 = 13; - - 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, " - + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " - + Contact.GROUPS + " 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.RELATIVE_FILE_PATH + " TEXT, " - + Message.SERVER_MSG_ID + " TEXT, " - + 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"); - } - if (oldVersion < 9 && newVersion >= 9) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_TIME + " NUMBER"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_PRESENCE + " TEXT"); - } - if (oldVersion < 10 && newVersion >= 10) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.RELATIVE_FILE_PATH + " TEXT"); - } - if (oldVersion < 11 && newVersion >= 11) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.GROUPS + " TEXT"); - db.execSQL("delete from "+Contact.TABLENAME); - db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL"); - } - if (oldVersion < 12 && newVersion >= 12) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.SERVER_MSG_ID + " TEXT"); - } - if (oldVersion < 13 && newVersion >= 13) { - db.execSQL("delete from "+Contact.TABLENAME); - db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL"); - } - } - - 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(); - int count = cursor.getInt(0); - cursor.close(); - return count; - } - - 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)); - } - cursor.close(); - 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()); - } - cursor.close(); - return list; - } - - public Conversation findConversation(final Account account, final Jid contactJid) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { account.getUuid(), - contactJid.toBareJid().toString() + "/%", - contactJid.toBareJid().toString() - }; - Cursor cursor = db.query(Conversation.TABLENAME, null, - Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID - + " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null); - if (cursor.getCount() == 0) - return null; - cursor.moveToFirst(); - Conversation conversation = Conversation.fromCursor(cursor); - cursor.close(); - return conversation; - } - - public void updateConversation(final Conversation conversation) { - final SQLiteDatabase db = this.getWritableDatabase(); - final 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 - } catch (RuntimeException 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(final Roster roster) { - final Account account = roster.getAccount(); - final 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().toString() }; - 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(); - Conversation conversation = Conversation.fromCursor(cursor); - cursor.close(); - return conversation; - } - - 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(); - Message message = Message.fromCursor(cursor); - cursor.close(); - return message; - } - - 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(); - Account account = Account.fromCursor(cursor); - cursor.close(); - return account; - } - - public List getImageMessages(Conversation conversation) { - ArrayList list = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor; - String[] selectionArgs = { conversation.getUuid(), String.valueOf(Message.TYPE_IMAGE) }; - cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=? AND "+Message.TYPE+"=?", selectionArgs, null, null,null); - if (cursor.getCount() > 0) { - cursor.moveToLast(); - do { - Message message = Message.fromCursor(cursor); - message.setConversation(conversation); - list.add(message); - } while (cursor.moveToPrevious()); - } - cursor.close(); - return list; - } -} diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java deleted file mode 100644 index c499d499..00000000 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ /dev/null @@ -1,525 +0,0 @@ -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.net.URL; -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.net.Uri; -import android.os.Environment; -import android.provider.MediaStore; -import android.util.Base64; -import android.util.Base64OutputStream; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -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.utils.ExifHelper; -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) { - String path = message.getRelativeFilePath(); - String extension; - if (path != null && !path.isEmpty()) { - String[] parts = path.split("\\."); - extension = "."+parts[parts.length - 1]; - } else { - if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_TEXT) { - extension = ".webp"; - } else { - extension = ""; - } - path = message.getUuid()+extension; - } - final boolean encrypted = !decrypted - && (message.getEncryption() == Message.ENCRYPTION_PGP - || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); - if (encrypted) { - return new DownloadableFile(getConversationsFileDirectory()+message.getUuid()+extension+".pgp"); - } else { - if (path.startsWith("/")) { - return new DownloadableFile(path); - } else { - if (message.getType() == Message.TYPE_FILE) { - return new DownloadableFile(getConversationsFileDirectory() + path); - } else { - return new DownloadableFile(getConversationsImageDirectory()+path); - } - } - } - } - - public static String getConversationsFileDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/"; - } - - public static String getConversationsImageDirectory() { - 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 String getOriginalPath(Uri uri) { - String path = null; - if (uri.getScheme().equals("file")) { - return uri.getPath(); - } else if (uri.toString().startsWith("content://media/")) { - String[] projection = {MediaStore.MediaColumns.DATA}; - Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri, - projection, null, null, null); - if (metaCursor != null) { - try { - if (metaCursor.moveToFirst()) { - path = metaCursor.getString(0); - } - } finally { - metaCursor.close(); - } - } - } - return path; - } - - public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { - try { - Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); - String mime = mXmppConnectionService.getContentResolver().getType(uri); - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); - message.setRelativeFilePath(message.getUuid() + "." + extension); - DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); - file.getParentFile().mkdirs(); - file.createNewFile(); - OutputStream os = new FileOutputStream(file); - InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri); - byte[] buffer = new byte[1024]; - int length; - while ((length = is.read(buffer)) > 0) { - os.write(buffer, 0, length); - } - os.flush(); - os.close(); - is.close(); - Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message)); - return file; - } catch (FileNotFoundException e) { - throw new FileCopyException(R.string.error_file_not_found); - } catch (IOException e) { - throw new FileCopyException(R.string.error_io_exception); - } - } - - public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) - throws FileCopyException { - return this.copyImageToPrivateStorage(message, image, 0); - } - - private DownloadableFile copyImageToPrivateStorage(Message message, - Uri image, int sampleSize) throws FileCopyException { - 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 FileCopyException(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 FileCopyException(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 FileCopyException(R.string.error_file_not_found); - } catch (IOException e) { - throw new FileCopyException(R.string.error_io_exception); - } catch (SecurityException e) { - throw new FileCopyException( - R.string.error_security_exception_during_image_copy); - } catch (OutOfMemoryError e) { - ++sampleSize; - if (sampleSize <= 3) { - return copyImageToPrivateStorage(message, image, sampleSize); - } else { - throw new FileCopyException(R.string.error_out_of_memory); - } - } - } - - private int getRotation(Uri image) { - try { - InputStream is = mXmppConnectionService.getContentResolver() - .openInputStream(image); - return ExifHelper.getOrientation(is); - } catch (FileNotFoundException e) { - return 0; - } - } - - 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 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) { - File file; - if (isAvatarCached(avatar)) { - file = new File(getAvatarPath(avatar.getFilename())); - } else { - String filename = getAvatarPath(avatar.getFilename()); - 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(); - String sha1sum = CryptoHelper.bytesToHex(digest.digest()); - if (sha1sum.equals(avatar.sha1sum)) { - file.renameTo(new File(filename)); - } 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; - } - } - avatar.size = file.length(); - return true; - } - - 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) { - if (image == null) { - return null; - } - 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) { - if (image == null) { - return null; - } - 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 void updateFileParams(Message message) { - updateFileParams(message,null); - } - - public void updateFileParams(Message message, URL url) { - DownloadableFile file = getFile(message); - if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - int imageHeight = options.outHeight; - int imageWidth = options.outWidth; - if (url == null) { - message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); - } else { - message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); - } - } else { - message.setBody(Long.toString(file.getSize())); - } - - } - - public class FileCopyException extends Exception { - private static final long serialVersionUID = -1010013599132881427L; - private int resId; - - public FileCopyException(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/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java deleted file mode 100644 index 6a457b17..00000000 --- a/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.persistance; - -public interface OnPhoneContactsMerged { - public void phoneContactsMerged(); -} diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java deleted file mode 100644 index 676a09c9..00000000 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ /dev/null @@ -1,23 +0,0 @@ -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/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java deleted file mode 100644 index 7412eb93..00000000 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ /dev/null @@ -1,295 +0,0 @@ -package eu.siacs.conversations.services; - -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 java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -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 eu.siacs.conversations.utils.UIHelper; - -public class AvatarService { - - private static final int FG_COLOR = 0xFFFAFAFA; - private static final int TRANSPARENT = 0x00000000; - private static final int PLACEHOLDER_COLOR = 0xFF202020; - - 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"; - - final private ArrayList sizes = new ArrayList<>(); - - protected XmppConnectionService mXmppConnectionService = null; - - public AvatarService(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { - final String KEY = key(contact, size); - Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (avatar != null || cachedOnly) { - return avatar; - } - if (contact.getProfilePhoto() != null) { - avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); - } - if (avatar == null && contact.getAvatar() != null) { - avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); - } - if (avatar == null) { - avatar = get(contact.getDisplayName(), size, cachedOnly); - } - this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); - return avatar; - } - - public void clear(Contact contact) { - synchronized (this.sizes) { - 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().toBareJid() + "_" - + contact.getJid() + "_" + String.valueOf(size); - } - - public Bitmap get(ListItem item, int size) { - return get(item,size,false); - } - - public Bitmap get(ListItem item, int size, boolean cachedOnly) { - if (item instanceof Contact) { - return get((Contact) item, size,cachedOnly); - } else if (item instanceof Bookmark) { - Bookmark bookmark = (Bookmark) item; - if (bookmark.getConversation() != null) { - return get(bookmark.getConversation(), size, cachedOnly); - } else { - return get(bookmark.getDisplayName(), size, cachedOnly); - } - } else { - return get(item.getDisplayName(), size, cachedOnly); - } - } - - public Bitmap get(Conversation conversation, int size) { - return get(conversation,size,false); - } - - public Bitmap get(Conversation conversation, int size, boolean cachedOnly) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - return get(conversation.getContact(), size, cachedOnly); - } else { - return get(conversation.getMucOptions(), size, cachedOnly); - } - } - - public void clear(Conversation conversation) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - clear(conversation.getContact()); - } else { - clear(conversation.getMucOptions()); - } - } - - private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) { - final String KEY = key(mucOptions, size); - Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (bitmap != null || cachedOnly) { - return bitmap; - } - final List users = new ArrayList<>(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(); - final String letter = name.isEmpty() ? "X" : name.substring(0,1); - final int color = UIHelper.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", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1, - size, size); - } - this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); - return bitmap; - } - - public void clear(MucOptions options) { - synchronized (this.sizes) { - 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; - } - avatar = mXmppConnectionService.getFileBackend().getAvatar( - account.getAvatar(), size); - if (avatar == null) { - avatar = get(account.getJid().toBareJid().toString(), size,false); - } - mXmppConnectionService.getBitmapCache().put(KEY, avatar); - return avatar; - } - - public void clear(Account account) { - synchronized (this.sizes) { - 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) { - return get(name,size,false); - } - - public Bitmap get(final String name, final int size, boolean cachedOnly) { - final String KEY = key(name, size); - Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); - if (bitmap != null || cachedOnly) { - return bitmap; - } - bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - final String trimmedName = name.trim(); - final String letter = trimmedName.isEmpty() ? "X" : trimmedName.substring(0,1); - final int color = UIHelper.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.getProfilePhoto() != null) { - uri = Uri.parse(contact.getProfilePhoto()); - } else if (contact.getAvatar() != null) { - uri = mXmppConnectionService.getFileBackend().getAvatarUri( - contact.getAvatar()); - } - if (uri != null) { - Bitmap bitmap = mXmppConnectionService.getFileBackend() - .cropCenter(uri, bottom - top, right - left); - if (bitmap != null) { - drawTile(canvas, bitmap, left, top, right, bottom); - return; - } - } - } - String name = contact != null ? contact.getDisplayName() : user.getName(); - final String letter = name.isEmpty() ? "X" : name.substring(0,1); - final int color = UIHelper.getColorForName(name); - 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); - } - -} diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java deleted file mode 100644 index dfbe9db7..00000000 --- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java +++ /dev/null @@ -1,24 +0,0 @@ -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/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java deleted file mode 100644 index f97077c4..00000000 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ /dev/null @@ -1,371 +0,0 @@ -package eu.siacs.conversations.services; - -import android.util.Log; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -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.generator.AbstractGenerator; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.jid.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { - - private final XmppConnectionService mXmppConnectionService; - - private final HashSet queries = new HashSet(); - private final ArrayList pendingQueries = new ArrayList(); - - public enum PagingOrder { - NORMAL, - REVERSE - }; - - public MessageArchiveService(final XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public void catchup(final Account account) { - long startCatchup = getLastMessageTransmitted(account); - long endCatchup = account.getXmppConnection().getLastSessionEstablished(); - if (startCatchup == 0) { - return; - } else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) { - startCatchup = endCatchup - Config.MAM_MAX_CATCHUP; - List conversations = mXmppConnectionService.getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) { - this.query(conversation,startCatchup); - } - } - } - final Query query = new Query(account, startCatchup, endCatchup); - this.queries.add(query); - this.execute(query); - } - - private long getLastMessageTransmitted(final Account account) { - long timestamp = 0; - for(final Conversation conversation : mXmppConnectionService.getConversations()) { - if (conversation.getAccount() == account) { - long tmp = conversation.getLastMessageTransmitted(); - if (tmp > timestamp) { - timestamp = tmp; - } - } - } - return timestamp; - } - - public Query query(final Conversation conversation) { - return query(conversation,conversation.getAccount().getXmppConnection().getLastSessionEstablished()); - } - - public Query query(final Conversation conversation, long end) { - return this.query(conversation,conversation.getLastMessageTransmitted(),end); - } - - public Query query(Conversation conversation, long start, long end) { - synchronized (this.queries) { - if (start > end) { - return null; - } - final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); - this.queries.add(query); - this.execute(query); - return query; - } - } - - public void executePendingQueries(final Account account) { - List pending = new ArrayList<>(); - synchronized(this.pendingQueries) { - for(Iterator iterator = this.pendingQueries.iterator(); iterator.hasNext();) { - Query query = iterator.next(); - if (query.getAccount() == account) { - pending.add(query); - iterator.remove(); - } - } - } - for(Query query : pending) { - this.execute(query); - } - } - - private void execute(final Query query) { - final Account account= query.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString()); - IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); - this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); - finalizeQuery(query); - } - } - }); - } else { - synchronized (this.pendingQueries) { - this.pendingQueries.add(query); - } - } - } - - private void finalizeQuery(Query query) { - synchronized (this.queries) { - this.queries.remove(query); - } - final Conversation conversation = query.getConversation(); - if (conversation != null) { - conversation.sort(); - if (conversation.setLastMessageTransmitted(query.getEnd())) { - this.mXmppConnectionService.databaseBackend.updateConversation(conversation); - } - conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); - if (query.hasCallback()) { - query.callback(); - } else { - this.mXmppConnectionService.updateConversationUi(); - } - } else { - for(Conversation tmp : this.mXmppConnectionService.getConversations()) { - if (tmp.getAccount() == query.getAccount()) { - tmp.sort(); - if (tmp.setLastMessageTransmitted(query.getEnd())) { - this.mXmppConnectionService.databaseBackend.updateConversation(tmp); - } - } - } - } - } - - public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { - synchronized (this.queries) { - for(Query query : queries) { - if (query.conversation == conversation) { - if (!query.hasCallback() && callback != null) { - query.setCallback(callback); - } - return true; - } - } - return false; - } - } - - public void processFin(Element fin) { - if (fin == null) { - return; - } - Query query = findQuery(fin.getAttribute("queryid")); - if (query == null) { - return; - } - boolean complete = fin.getAttributeAsBoolean("complete"); - Element set = fin.findChild("set","http://jabber.org/protocol/rsm"); - Element last = set == null ? null : set.findChild("last"); - Element first = set == null ? null : set.findChild("first"); - Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first; - boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES; - if (complete || relevant == null || abort) { - this.finalizeQuery(query); - Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages"); - } else { - final Query nextQuery; - if (query.getPagingOrder() == PagingOrder.NORMAL) { - nextQuery = query.next(last == null ? null : last.getContent()); - } else { - nextQuery = query.prev(first == null ? null : first.getContent()); - } - this.execute(nextQuery); - this.finalizeQuery(query); - synchronized (this.queries) { - this.queries.remove(query); - this.queries.add(nextQuery); - } - } - } - - public Query findQuery(String id) { - if (id == null) { - return null; - } - synchronized (this.queries) { - for(Query query : this.queries) { - if (query.getQueryId().equals(id)) { - return query; - } - } - return null; - } - } - - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) { - this.catchup(account); - } - } - - public class Query { - private int totalCount = 0; - private int messageCount = 0; - private long start; - private long end; - private String queryId; - private String reference = null; - private Account account; - private Conversation conversation; - private PagingOrder pagingOrder = PagingOrder.NORMAL; - private XmppConnectionService.OnMoreMessagesLoaded callback = null; - - - public Query(Conversation conversation, long start, long end) { - this(conversation.getAccount(), start, end); - this.conversation = conversation; - } - - public Query(Conversation conversation, long start, long end, PagingOrder order) { - this(conversation,start,end); - this.pagingOrder = order; - } - - public Query(Account account, long start, long end) { - this.account = account; - this.start = start; - this.end = end; - this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); - } - - private Query page(String reference) { - Query query = new Query(this.account,this.start,this.end); - query.reference = reference; - query.conversation = conversation; - query.totalCount = totalCount; - query.callback = callback; - return query; - } - - public Query next(String reference) { - Query query = page(reference); - query.pagingOrder = PagingOrder.NORMAL; - return query; - } - - public Query prev(String reference) { - Query query = page(reference); - query.pagingOrder = PagingOrder.REVERSE; - return query; - } - - public String getReference() { - return reference; - } - - public PagingOrder getPagingOrder() { - return this.pagingOrder; - } - - public String getQueryId() { - return queryId; - } - - public Jid getWith() { - return conversation == null ? null : conversation.getJid().toBareJid(); - } - - public boolean muc() { - return conversation != null && conversation.getMode() == Conversation.MODE_MULTI; - } - - public long getStart() { - return start; - } - - public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) { - this.callback = callback; - } - - public void callback() { - if (this.callback != null) { - this.callback.onMoreMessagesLoaded(messageCount,conversation); - if (messageCount == 0) { - this.callback.informUser(R.string.no_more_history_on_server); - } - } - } - - public long getEnd() { - return end; - } - - public Conversation getConversation() { - return conversation; - } - - public Account getAccount() { - return this.account; - } - - public void incrementTotalCount() { - this.totalCount++; - } - - public void incrementMessageCount() { - this.messageCount++; - } - - public int getTotalCount() { - return this.totalCount; - } - - public int getMessageCount() { - return this.messageCount; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (this.muc()) { - builder.append("to="+this.getWith().toString()); - } else { - builder.append("with="); - if (this.getWith() == null) { - builder.append("*"); - } else { - builder.append(getWith().toString()); - } - } - builder.append(", start="); - builder.append(AbstractGenerator.getTimestamp(this.start)); - builder.append(", end="); - builder.append(AbstractGenerator.getTimestamp(this.end)); - if (this.reference!=null) { - if (this.pagingOrder == PagingOrder.NORMAL) { - builder.append(", after="); - } else { - builder.append(", before="); - } - builder.append(this.reference); - } - return builder.toString(); - } - - public boolean hasCallback() { - return this.callback != null; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java deleted file mode 100644 index 642021bf..00000000 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ /dev/null @@ -1,559 +0,0 @@ -package eu.siacs.conversations.services; - -import android.annotation.SuppressLint; -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.graphics.Bitmap; -import android.net.Uri; -import android.os.Build; -import android.os.PowerManager; -import android.os.SystemClock; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationCompat.BigPictureStyle; -import android.support.v4.app.NotificationCompat.Builder; -import android.support.v4.app.TaskStackBuilder; -import android.text.Html; -import android.util.DisplayMetrics; -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONObject; - -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.tzur.conversations.Settings; -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.ConversationActivity; -import eu.siacs.conversations.ui.ManageAccountActivity; -import eu.siacs.conversations.ui.TimePreference; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.XmppConnection; - -public class NotificationService { - - private final XmppConnectionService mXmppConnectionService; - - private final LinkedHashMap> notifications = new LinkedHashMap<>(); - - public static final int NOTIFICATION_ID = 0x2342; - public static final int FOREGROUND_NOTIFICATION_ID = 0x8899; - public static final int ERROR_NOTIFICATION_ID = 0x5678; - - private Conversation mOpenConversation; - private boolean mIsInForeground; - private long mLastNotification; - - public NotificationService(final XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public boolean notify(final Message message) { - return (message.getStatus() == Message.STATUS_RECEIVED) - && notificationsEnabled() - && !message.getConversation().isMuted() - && (message.getConversation().getMode() == Conversation.MODE_SINGLE - || conferenceNotificationsEnabled() - || wasHighlightedOrPrivate(message) - ); - } - - public void notifyPebble(final Message message) { - final Intent i = new Intent("com.getpebble.action.SEND_NOTIFICATION"); - - final Conversation conversation = message.getConversation(); - final JSONObject jsonData = new JSONObject(new HashMap(2) {{ - put("title", conversation.getName()); - put("body", message.getBody()); - }}); - final String notificationData = new JSONArray().put(jsonData).toString(); - - i.putExtra("messageType", "PEBBLE_ALERT"); - i.putExtra("sender", "Conversations"); /* XXX: Shouldn't be hardcoded, e.g., AbstractGenerator.APP_NAME); */ - i.putExtra("notificationData", notificationData); - - mXmppConnectionService.sendBroadcast(i); - } - - - public boolean notificationsEnabled() { - return mXmppConnectionService.getPreferences().getBoolean("show_notification", true); - } - - public boolean isQuietHours() { - if (!mXmppConnectionService.getPreferences().getBoolean("enable_quiet_hours", false)) { - return false; - } - final long startTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; - final long endTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; - final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY; - - if (endTime < startTime) { - return nowTime > startTime || nowTime < endTime; - } else { - return nowTime > startTime && nowTime < endTime; - } - } - - public boolean conferenceNotificationsEnabled() { - return mXmppConnectionService.getPreferences().getBoolean("always_notify_in_conference", false); - } - - @SuppressLint("NewApi") - @SuppressWarnings("deprecation") - private boolean isInteractive() { - final PowerManager pm = (PowerManager) mXmppConnectionService - .getSystemService(Context.POWER_SERVICE); - - final boolean isScreenOn; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - isScreenOn = pm.isScreenOn(); - } else { - isScreenOn = pm.isInteractive(); - } - - return isScreenOn; - } - - public void push(final Message message) { - if (!notify(message)) { - return; - } - - final boolean isScreenOn = isInteractive(); - - if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) { - return; - } - - synchronized (notifications) { - final String conversationUuid = message.getConversationUuid(); - if (notifications.containsKey(conversationUuid)) { - notifications.get(conversationUuid).add(message); - } else { - final ArrayList mList = new ArrayList<>(); - mList.add(message); - notifications.put(conversationUuid, mList); - } - final Account account = message.getConversation().getAccount(); - final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) - && !account.inGracePeriod() - && !this.inMiniGracePeriod(account); - updateNotification(doNotify); - if (doNotify) { - notifyPebble(message); - } - } - } - - public void clear() { - synchronized (notifications) { - notifications.clear(); - updateNotification(false); - } - } - - public void clear(final Conversation conversation) { - synchronized (notifications) { - notifications.remove(conversation.getUuid()); - updateNotification(false); - } - } - - private void setNotificationColor(final Builder mBuilder) { - mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary)); - } - - private void updateNotification(final boolean notify) { - final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService - .getSystemService(Context.NOTIFICATION_SERVICE); - final SharedPreferences preferences = mXmppConnectionService.getPreferences(); - - final String ringtone = preferences.getString("notification_ringtone", null); - final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true); - - if (notifications.size() == 0) { - notificationManager.cancel(NOTIFICATION_ID); - } else { - if (notify) { - this.markLastNotification(); - } - final Builder mBuilder; - if (notifications.size() == 1) { - mBuilder = buildSingleConversations(notify); - } else { - mBuilder = buildMultipleConversation(); - } - if (notify && !isQuietHours()) { - if (vibrate) { - final int dat = 70; - final long[] pattern = {0, 3 * dat, dat, dat}; - mBuilder.setVibrate(pattern); - } - if (ringtone != null) { - mBuilder.setSound(Uri.parse(ringtone)); - } - } - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setCategory(Notification.CATEGORY_MESSAGE); - } - setNotificationColor(mBuilder); - mBuilder.setDefaults(0); - mBuilder.setSmallIcon(R.drawable.ic_notification); - mBuilder.setDeleteIntent(createDeleteIntent()); - mBuilder.setLights(Settings.LED_COLOR, 2000, 4000); - final Notification notification = mBuilder.build(); - notificationManager.notify(NOTIFICATION_ID, notification); - } - } - - private Builder buildMultipleConversation() { - final Builder mBuilder = new NotificationCompat.Builder( - mXmppConnectionService); - final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(notifications.size() - + " " - + mXmppConnectionService - .getString(R.string.unread_conversations)); - final StringBuilder names = new StringBuilder(); - Conversation conversation = null; - for (final ArrayList messages : notifications.values()) { - if (messages.size() > 0) { - conversation = messages.get(0).getConversation(); - final String name = conversation.getName(); - style.addLine(Html.fromHtml("" + name + " " - + UIHelper.getMessagePreview(mXmppConnectionService,messages.get(0)).first)); - 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)); - } - return mBuilder; - } - - private Builder buildSingleConversations(final boolean notify) { - final Builder mBuilder = new NotificationCompat.Builder( - mXmppConnectionService); - final ArrayList messages = notifications.values().iterator().next(); - if (messages.size() >= 1) { - final Conversation conversation = messages.get(0).getConversation(); - mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() - .get(conversation, getPixel(64))); - mBuilder.setContentTitle(conversation.getName()); - Message message; - if ((message = getImage(messages)) != null) { - modifyForImage(mBuilder, message, messages, notify); - } else { - modifyForTextOnly(mBuilder, messages, notify); - } - if ((message = getFirstDownloadableMessage(messages)) != null) { - mBuilder.addAction( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? - R.drawable.ic_file_download_white_24dp : R.drawable.ic_action_download, - mXmppConnectionService.getResources().getString(R.string.download_x_file, - UIHelper.getFileDescriptionString(mXmppConnectionService, message)), - createDownloadIntent(message) - ); - } - if ((message = getFirstLocationMessage(messages)) != null) { - mBuilder.addAction(R.drawable.ic_room_white_24dp, - mXmppConnectionService.getString(R.string.show_location), - createShowLocationIntent(message)); - } - mBuilder.setContentIntent(createContentIntent(conversation)); - } - return mBuilder; - } - - private void modifyForImage(final Builder builder, final Message message, - final ArrayList messages, final boolean notify) { - try { - final Bitmap bitmap = mXmppConnectionService.getFileBackend() - .getThumbnail(message, getPixel(288), false); - final ArrayList tmp = new ArrayList<>(); - for (final Message msg : messages) { - if (msg.getType() == Message.TYPE_TEXT - && msg.getDownloadable() == null) { - tmp.add(msg); - } - } - final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); - bigPictureStyle.bigPicture(bitmap); - if (tmp.size() > 0) { - bigPictureStyle.setSummaryText(getMergedBodies(tmp)); - builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService,tmp.get(0)).first); - } else { - builder.setContentText(mXmppConnectionService.getString( - R.string.received_x_file, - UIHelper.getFileDescriptionString(mXmppConnectionService,message))); - } - builder.setStyle(bigPictureStyle); - } catch (final FileNotFoundException e) { - modifyForTextOnly(builder, messages, notify); - } - } - - private void modifyForTextOnly(final Builder builder, - final ArrayList messages, final boolean notify) { - builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); - builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(0)).first); - if (notify) { - builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(messages.size() - 1)).first); - } - } - - private Message getImage(final Iterable messages) { - for (final Message message : messages) { - if (message.getType() == Message.TYPE_IMAGE - && message.getDownloadable() == null - && message.getEncryption() != Message.ENCRYPTION_PGP) { - return message; - } - } - return null; - } - - private Message getFirstDownloadableMessage(final Iterable messages) { - for (final Message message : messages) { - if ((message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) && - message.getDownloadable() != null) { - return message; - } - } - return null; - } - - private Message getFirstLocationMessage(final Iterable messages) { - for(final Message message : messages) { - if (GeoHelper.isGeoUri(message.getBody())) { - return message; - } - } - return null; - } - - private CharSequence getMergedBodies(final ArrayList messages) { - final StringBuilder text = new StringBuilder(); - for (int i = 0; i < messages.size(); ++i) { - text.append(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(i)).first); - if (i != messages.size() - 1) { - text.append("\n"); - } - } - return text.toString(); - } - - private PendingIntent createShowLocationIntent(final Message message) { - Iterable intents = GeoHelper.createGeoIntentsFromMessage(message); - for(Intent intent : intents) { - if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) { - return PendingIntent.getActivity(mXmppConnectionService,18,intent,PendingIntent.FLAG_UPDATE_CURRENT); - } - } - return createOpenConversationsIntent(); - } - - private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) { - final TaskStackBuilder stackBuilder = TaskStackBuilder - .create(mXmppConnectionService); - stackBuilder.addParentStack(ConversationActivity.class); - - final Intent viewConversationIntent = new Intent(mXmppConnectionService, - ConversationActivity.class); - if (downloadMessageUuid != null) { - viewConversationIntent.setAction(ConversationActivity.ACTION_DOWNLOAD); - } else { - viewConversationIntent.setAction(Intent.ACTION_VIEW); - } - if (conversationUuid != null) { - viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid); - viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); - } - if (downloadMessageUuid != null) { - viewConversationIntent.putExtra(ConversationActivity.MESSAGE, downloadMessageUuid); - } - - stackBuilder.addNextIntent(viewConversationIntent); - - return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createDownloadIntent(final Message message) { - return createContentIntent(message.getConversationUuid(), message.getUuid()); - } - - private PendingIntent createContentIntent(final Conversation conversation) { - return createContentIntent(conversation.getUuid(), null); - } - - private PendingIntent createDeleteIntent() { - final Intent intent = new Intent(mXmppConnectionService, - XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); - return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); - } - - private PendingIntent createDisableForeground() { - final Intent intent = new Intent(mXmppConnectionService, - XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND); - return PendingIntent.getService(mXmppConnectionService, 34, intent, 0); - } - - private PendingIntent createTryAgainIntent() { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN); - return PendingIntent.getService(mXmppConnectionService, 45, intent, 0); - } - - private PendingIntent createDisableAccountIntent(final Account account) { - final Intent intent = new Intent(mXmppConnectionService,XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_DISABLE_ACCOUNT); - intent.putExtra("account",account.getJid().toBareJid().toString()); - return PendingIntent.getService(mXmppConnectionService,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); - } - - private boolean wasHighlightedOrPrivate(final Message message) { - final String nick = message.getConversation().getMucOptions().getActualNick(); - final Pattern highlight = generateNickHighlightPattern(nick); - if (message.getBody() == null || nick == null) { - return false; - } - final Matcher m = highlight.matcher(message.getBody()); - return (m.find() || message.getType() == Message.TYPE_PRIVATE); - } - - private static Pattern generateNickHighlightPattern(final 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(final Conversation conversation) { - this.mOpenConversation = conversation; - } - - public void setIsInForeground(final boolean foreground) { - this.mIsInForeground = foreground; - } - - private int getPixel(final int dp) { - final DisplayMetrics metrics = mXmppConnectionService.getResources() - .getDisplayMetrics(); - return ((int) (dp * metrics.density)); - } - - private void markLastNotification() { - this.mLastNotification = SystemClock.elapsedRealtime(); - } - - private boolean inMiniGracePeriod(final Account account) { - final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD - : Config.MINI_GRACE_PERIOD * 2; - return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); - } - - public Notification createForegroundNotification() { - final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); - - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service)); - mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations)); - mBuilder.setContentIntent(createOpenConversationsIntent()); - mBuilder.setWhen(0); - mBuilder.setPriority(NotificationCompat.PRIORITY_MIN); - final int cancelIcon; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setCategory(Notification.CATEGORY_SERVICE); - mBuilder.setSmallIcon(R.drawable.ic_import_export_white_48dp); - cancelIcon = R.drawable.ic_cancel_white_24dp; - } else { - mBuilder.setSmallIcon(R.drawable.ic_stat_communication_import_export); - cancelIcon = R.drawable.ic_action_cancel; - } - mBuilder.addAction(cancelIcon, - mXmppConnectionService.getString(R.string.disable_foreground_service), - createDisableForeground()); - setNotificationColor(mBuilder); - return mBuilder.build(); - } - - private PendingIntent createOpenConversationsIntent() { - return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService,ConversationActivity.class),0); - } - - public void updateErrorNotification() { - final NotificationManager mNotificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); - final List errors = new ArrayList<>(); - for (final Account account : mXmppConnectionService.getAccounts()) { - if (account.hasErrorStatus()) { - errors.add(account); - } - } - final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); - if (errors.size() == 0) { - mNotificationManager.cancel(ERROR_NOTIFICATION_ID); - return; - } else if (errors.size() == 1) { - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account)); - mBuilder.setContentText(errors.get(0).getJid().toBareJid().toString()); - } else { - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts)); - mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix)); - } - mBuilder.addAction(R.drawable.ic_autorenew_white_24dp, - mXmppConnectionService.getString(R.string.try_again), - createTryAgainIntent()); - if (errors.size() == 1) { - mBuilder.addAction(R.drawable.ic_block_white_24dp, - mXmppConnectionService.getString(R.string.disable_account), - createDisableAccountIntent(errors.get(0))); - } - mBuilder.setOngoing(true); - //mBuilder.setLights(0xffffffff, 2000, 4000); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setSmallIcon(R.drawable.ic_warning_white_36dp); - } else { - mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning); - } - final TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService); - stackBuilder.addParentStack(ConversationActivity.class); - - final Intent manageAccountsIntent = new Intent(mXmppConnectionService,ManageAccountActivity.class); - stackBuilder.addNextIntent(manageAccountsIntent); - - final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT); - - mBuilder.setContentIntent(resultPendingIntent); - mNotificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build()); - } -} diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java deleted file mode 100644 index fc670efd..00000000 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ /dev/null @@ -1,2434 +0,0 @@ -package eu.siacs.conversations.services; - -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.Looper; -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; - -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.SessionStatus; - -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpServiceConnection; - -import java.math.BigInteger; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Hashtable; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; - -import de.duenndns.ssl.MemorizingTrustManager; -import de.tzur.conversations.Settings; -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.Blockable; -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.DownloadablePlaceholder; -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.Xmlns; -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.OnMessagePacketReceived; -import eu.siacs.conversations.xmpp.OnPresencePacketReceived; -import eu.siacs.conversations.xmpp.OnStatusChanged; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.forms.Field; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; -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; - -public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener { - - public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; - public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground"; - private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; - public static final String ACTION_TRY_AGAIN = "try_again"; - public static final String ACTION_DISABLE_ACCOUNT = "disable_account"; - 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 final IBinder mBinder = new XmppConnectionBinder(); - private final List conversations = new CopyOnWriteArrayList<>(); - private final FileObserver fileObserver = new FileObserver( - FileBackend.getConversationsImageDirectory()) { - - @Override - public void onEvent(int event, String path) { - if (event == FileObserver.DELETE) { - markFileDeleted(path.split("\\.")[0]); - } - } - }; - private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { - - @Override - public void onJinglePacketReceived(Account account, JinglePacket packet) { - mJingleConnectionManager.deliverPacket(account, packet); - } - }; - private final OnBindListener mOnBindListener = new OnBindListener() { - - @Override - public void onBind(final Account account) { - account.getRoster().clearPresences(); - account.pendingConferenceJoins.clear(); - account.pendingConferenceLeaves.clear(); - fetchRosterFromServer(account); - fetchBookmarks(account); - sendPresence(account); - connectMultiModeConversations(account); - updateConversationUi(); - } - }; - private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { - - @Override - public void onMessageAcknowledged(Account account, String uuid) { - for (final Conversation conversation : getConversations()) { - if (conversation.getAccount() == account) { - Message message = conversation.findUnsentMessageWithUuid(uuid); - if (message != null) { - markMessage(message, Message.STATUS_SEND); - if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { - databaseBackend.updateConversation(conversation); - } - } - } - } - } - }; - private final IqGenerator mIqGenerator = new IqGenerator(this); - public DatabaseBackend databaseBackend; - public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { - - @Override - public void onContactStatusChanged(Contact contact, boolean online) { - Conversation conversation = find(getConversations(), contact); - if (conversation != null) { - if (online && contact.getPresences().size() > 1) { - conversation.endOtrIfNeeded(); - } else { - conversation.resetOtrSession(); - } - if (online && (contact.getPresences().size() == 1)) { - sendUnsentMessages(conversation); - } - } - } - }; - private FileBackend fileBackend = new FileBackend(this); - private MemorizingTrustManager mMemorizingTrustManager; - private NotificationService mNotificationService = new NotificationService( - this); - private OnMessagePacketReceived mMessageParser = new MessageParser(this); - private OnPresencePacketReceived 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 JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( - this); - private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( - this); - private AvatarService mAvatarService = new AvatarService(this); - private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); - private OnConversationUpdate mOnConversationUpdate = null; - private Integer convChangedListenerCount = 0; - private OnAccountUpdate mOnAccountUpdate = null; - private OnStatusChanged statusListener = new OnStatusChanged() { - - @Override - public void onStatusChanged(Account account) { - XmppConnection connection = account.getXmppConnection(); - if (mOnAccountUpdate != null) { - mOnAccountUpdate.onAccountUpdate(); - } - if (account.getStatus() == Account.State.ONLINE) { - for (Conversation conversation : account.pendingConferenceLeaves) { - leaveMuc(conversation); - } - for (Conversation conversation : account.pendingConferenceJoins) { - joinMuc(conversation); - } - mMessageArchiveService.executePendingQueries(account); - mJingleConnectionManager.cancelInTransmission(); - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getAccount() == account) { - conversation.startOtrIfNeeded(); - sendUnsentMessages(conversation); - } - } - if (connection != null && connection.getFeatures().csi()) { - if (checkListeners()) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + " sending csi//inactive"); - connection.sendInactive(); - } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + " sending csi//active"); - connection.sendActive(); - } - } - syncDirtyContacts(account); - scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode()); - } else if (account.getStatus() == Account.State.OFFLINE) { - resetSendingToWaiting(account); - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - int timeToReconnect = mRandom.nextInt(50) + 10; - scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode()); - } - } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { - databaseBackend.updateAccount(account); - reconnectAccount(account, true); - } else if ((account.getStatus() != Account.State.CONNECTING) - && (account.getStatus() != Account.State.NO_INTERNET)) { - if (connection != null) { - int next = connection.getTimeToNextAttempt(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": error connecting account. try again in " - + next + "s for the " - + (connection.getAttempt() + 1) + " time"); - scheduleWakeUpCall(next,account.getUuid().hashCode()); - } - } - getNotificationService().updateErrorNotification(); - } - }; - private int accountChangedListenerCount = 0; - private OnRosterUpdate mOnRosterUpdate = null; - private OnUpdateBlocklist mOnUpdateBlocklist = null; - private int updateBlocklistListenerCount = 0; - private int rosterChangedListenerCount = 0; - private OnMucRosterUpdate mOnMucRosterUpdate = null; - private int mucRosterChangedListenerCount = 0; - private SecureRandom mRandom; - private OpenPgpServiceConnection pgpServiceConnection; - private PgpEngine mPgpEngine = null; - private WakeLock wakeLock; - private PowerManager pm; - private LruCache mBitmapCache; - private Thread mPhoneContactMergerThread; - - private boolean mRestoredFromDatabase = false; - public boolean areMessagesInitialized() { - return this.mRestoredFromDatabase; - } - - 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 void attachLocationToConversation(final Conversation conversation, - final Uri uri, - final UiCallback callback) { - int encryption = conversation.getNextEncryption(forceEncryption()); - if (encryption == Message.ENCRYPTION_PGP) { - encryption = Message.ENCRYPTION_DECRYPTED; - } - Message message = new Message(conversation,uri.toString(),encryption); - if (conversation.getNextCounterpart() != null) { - message.setCounterpart(conversation.getNextCounterpart()); - } - if (encryption == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message,callback); - } else { - callback.success(message); - } - } - - public void attachFileToConversation(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.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_FILE); - message.setStatus(Message.STATUS_OFFERED); - String path = getFileBackend().getOriginalPath(uri); - if (path!=null) { - message.setRelativeFilePath(path); - getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } else { - new Thread(new Runnable() { - @Override - public void run() { - try { - getFileBackend().copyFileToPrivateStorage(message, uri); - getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (FileBackend.FileCopyException e) { - callback.error(e.getResId(),message); - } - } - }).start(); - - } - } - - public void 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.setCounterpart(conversation.getNextCounterpart()); - 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 (final FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }).start(); - } - - public Conversation find(Bookmark bookmark) { - return find(bookmark.getAccount(), bookmark.getJid()); - } - - public Conversation find(final Account account, final Jid jid) { - return find(getConversations(), account, jid); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - final String action = intent == null ? null : intent.getAction(); - if (action != null) { - switch (action) { - case ACTION_MERGE_PHONE_CONTACTS: - if (mRestoredFromDatabase) { - PhoneHelper.loadPhoneContacts(getApplicationContext(), - new CopyOnWriteArrayList(), - this); - } - return START_STICKY; - case Intent.ACTION_SHUTDOWN: - logoutAndSave(); - return START_NOT_STICKY; - case ACTION_CLEAR_NOTIFICATION: - mNotificationService.clear(); - break; - case ACTION_DISABLE_FOREGROUND: - getPreferences().edit().putBoolean("keep_foreground_service",false).commit(); - toggleForegroundService(); - break; - case ACTION_TRY_AGAIN: - for(Account account : accounts) { - if (account.hasErrorStatus()) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.resetAttemptCount(); - } - } - } - break; - case ACTION_DISABLE_ACCOUNT: - try { - String jid = intent.getStringExtra("account"); - Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid)); - if (account != null) { - account.setOption(Account.OPTION_DISABLED,true); - updateAccount(account); - } - } catch (final InvalidJidException ignored) { - break; - } - break; - } - } - this.wakeLock.acquire(); - - for (Account account : accounts) { - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - if (!hasInternetConnection()) { - account.setStatus(Account.State.NO_INTERNET); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } else { - if (account.getStatus() == Account.State.NO_INTERNET) { - account.setStatus(Account.State.OFFLINE); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } - if (account.getStatus() == Account.State.ONLINE) { - long lastReceived = account.getXmppConnection().getLastPacketReceived(); - long lastSent = account.getXmppConnection().getLastPingSent(); - long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; - long msToNextPing = (Math.max(lastReceived,lastSent) + pingInterval) - SystemClock.elapsedRealtime(); - if (lastSent > lastReceived && (lastSent + Config.PING_TIMEOUT * 1000) < SystemClock.elapsedRealtime()) { - Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": ping timeout"); - this.reconnectAccount(account, true); - } else if (msToNextPing <= 0) { - account.getXmppConnection().sendPing(); - Log.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping"); - this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode()); - } else { - this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); - } - } else if (account.getStatus() == Account.State.OFFLINE) { - reconnectAccount(account,true); - } else if (account.getStatus() == Account.State.CONNECTING) { - long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000); - if (timeout < 0) { - Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting"); - reconnectAccount(account, true); - } else { - scheduleWakeUpCall((int) timeout,account.getUuid().hashCode()); - } - } else { - if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { - reconnectAccount(account, true); - } - } - - } - if (mOnAccountUpdate != null) { - mOnAccountUpdate.onAccountUpdate(); - } - } - } - /*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE); - if (!pm.isScreenOn()) { - removeStaleListeners(); - }*/ - if (wakeLock.isHeld()) { - try { - wakeLock.release(); - } catch (final RuntimeException ignored) { - } - } - return START_STICKY; - } - - public boolean hasInternetConnection() { - ConnectivityManager cm = (ConnectivityManager) getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - return activeNetwork != null && activeNetwork.isConnected(); - } - - /** - * check whether we are allowed to download at the moment - */ - public boolean isDownloadAllowedInConnection() { - if (Settings.DOWNLOAD_ONLY_WLAN) { - return isWifiConnected(); - } - return true; - } - - /** - * check whether wifi is connected - */ - public boolean isWifiConnected() { - ConnectivityManager cm = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo niWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - return niWifi.isConnected(); - } - - @SuppressLint("TrulyRandom") - @Override - public void onCreate() { - ExceptionHelper.init(getApplicationContext()); - PRNGFixes.apply(); - this.mRandom = new SecureRandom(); - this.mMemorizingTrustManager = new MemorizingTrustManager( - getApplicationContext()); - - final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - final int cacheSize = maxMemory / 8; - this.mBitmapCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(final String key, final Bitmap bitmap) { - return bitmap.getByteCount() / 1024; - } - }; - - this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); - this.accounts = databaseBackend.getAccounts(); - - for (final Account account : this.accounts) { - account.initOtrEngine(this); - } - restoreFromDatabase(); - - 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"); - toggleForegroundService(); - } - - public void toggleForegroundService() { - if (getPreferences().getBoolean("keep_foreground_service",false)) { - startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification()); - } else { - stopForeground(true); - } - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - if (!getPreferences().getBoolean("keep_foreground_service",false)) { - this.logoutAndSave(); - } - } - - private void logoutAndSave() { - for (final 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, int requestCode) { - final long timeToWake = SystemClock.elapsedRealtime() + (seconds + 1) * 1000; - - Context context = getApplicationContext(); - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - - Intent intent = new Intent(context, EventReceiver.class); - intent.setAction("ping"); - PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0); - alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent); - } - - public XmppConnection createConnection(final Account account) { - final SharedPreferences sharedPref = getPreferences(); - account.setResource(sharedPref.getString("resource", "mobile") - .toLowerCase(Locale.getDefault())); - final 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); - connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); - return connection; - } - - public void sendChatState(Conversation conversation) { - if (sendChatStates()) { - MessagePacket packet = mMessageGenerator.generateChatState(conversation); - sendMessagePacket(conversation.getAccount(), packet); - } - } - - public void sendMessage(final Message message) { - final Account account = message.getConversation().getAccount(); - account.deactivateGracePeriod(); - final Conversation conv = message.getConversation(); - MessagePacket packet = null; - boolean saveInDb = true; - boolean send = false; - if (account.getStatus() == Account.State.ONLINE - && account.getXmppConnection() != null) { - if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { - if (message.getCounterpart() != null) { - if (message.getEncryption() == Message.ENCRYPTION_OTR) { - if (!conv.hasValidOtrSession()) { - conv.startOtrSession(message.getCounterpart().getResourcepart(),true); - message.setStatus(Message.STATUS_WAITING); - } else if (conv.hasValidOtrSession() - && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { - mJingleConnectionManager - .createNewConnection(message); - } - } else { - mJingleConnectionManager.createNewConnection(message); - } - } else { - if (message.getEncryption() == Message.ENCRYPTION_OTR) { - conv.startOtrIfNeeded(); - } - message.setStatus(Message.STATUS_WAITING); - } - } else { - if (message.getEncryption() == Message.ENCRYPTION_OTR) { - if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) { - conv.startOtrSession(message.getCounterpart().getResourcepart(), true); - message.setStatus(Message.STATUS_WAITING); - } else if (conv.hasValidOtrSession()) { - if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { - packet = mMessageGenerator.generateOtrChat(message); - send = true; - } else { - message.setStatus(Message.STATUS_WAITING); - conv.startOtrIfNeeded(); - } - } else { - message.setStatus(Message.STATUS_WAITING); - } - } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - message.getConversation().endOtrIfNeeded(); - message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - markMessage(message,Message.STATUS_SEND_FAILED); - } - }); - packet = mMessageGenerator.generatePgpChat(message); - send = true; - } else { - message.getConversation().endOtrIfNeeded(); - message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - markMessage(message,Message.STATUS_SEND_FAILED); - } - }); - 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.getCounterpart() != null) { - conv.startOtrSession(message.getCounterpart().getResourcepart(), false); - } - } - } - - } - conv.add(message); - if (saveInDb) { - if (message.getEncryption() == Message.ENCRYPTION_NONE - || saveEncryptedMessages()) { - databaseBackend.createMessage(message); - } - } - if ((send) && (packet != null)) { - if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { - if (this.sendChatStates()) { - packet.addChild(ChatState.toElement(conv.getOutgoingChatState())); - } - } - sendMessagePacket(account, packet); - } - updateConversationUi(); - } - - private void sendUnsentMessages(final Conversation conversation) { - conversation.findWaitingMessages(new Conversation.OnMessageFound() { - - @Override - public void onMessageFound(Message message) { - resendMessage(message); - } - }); - } - - private void resendMessage(final 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.getCounterpart() != null) - && (presences.has(message.getCounterpart().getResourcepart()))) { - message.getConversation().startOtrSession(message.getCounterpart().getResourcepart(), true); - } else { - if (presences.size() == 1) { - String presence = presences.asStringArray()[0]; - message.getConversation().startOtrSession(presence, true); - } - } - } else { - if (message.getConversation().getOtrSession() - .getSessionStatus() == SessionStatus.ENCRYPTED) { - try { - message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID())); - if (message.getType() == Message.TYPE_TEXT) { - packet = mMessageGenerator.generateOtrChat(message, - true); - } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { - mJingleConnectionManager.createNewConnection(message); - } - } catch (final InvalidJidException ignored) { - - } - } - } - } 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 || message.getType() == Message.TYPE_FILE) { - Contact contact = message.getConversation().getContact(); - Presences presences = contact.getPresences(); - if ((message.getCounterpart() != null) - && (presences.has(message.getCounterpart().getResourcepart()))) { - markMessage(message, Message.STATUS_OFFERED); - mJingleConnectionManager.createNewConnection(message); - } else { - if (presences.size() == 1) { - String presence = presences.asStringArray()[0]; - try { - message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence)); - } catch (InvalidJidException e) { - return; - } - 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); - } - if (message.getConversation().setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { - if (this.sendChatStates()) { - packet.addChild(ChatState.toElement(message.getConversation().getOutgoingChatState())); - } - } - sendMessagePacket(account, packet); - } - } - - public void fetchRosterFromServer(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); - if (!"".equals(account.getRosterVersion())) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": fetching roster version " + account.getRosterVersion()); - } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); - } - iqPacket.query(Xmlns.ROSTER).setAttribute("ver", - account.getRosterVersion()); - account.getXmppConnection().sendIqPacket(iqPacket, mIqParser); - } - - public void fetchBookmarks(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); - final Element query = iqPacket.query("jabber:iq:private"); - query.addChild("storage", "storage:bookmarks"); - final OnIqPacketReceived callback = new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final Element query = packet.query(); - final List bookmarks = new CopyOnWriteArrayList<>(); - final Element storage = query.findChild("storage", - "storage:bookmarks"); - if (storage != null) { - for (final Element item : storage.getChildren()) { - if (item.getName().equals("conference")) { - final Bookmark bookmark = Bookmark.parse(item, account); - bookmarks.add(bookmark); - Conversation conversation = find(bookmark); - if (conversation != null) { - conversation.setBookmark(bookmark); - } else if (bookmark.autojoin() && bookmark.getJid() != null) { - 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); - } - - public void onPhoneContactsLoaded(final List phoneContacts) { - if (mPhoneContactMergerThread != null) { - mPhoneContactMergerThread.interrupt(); - } - mPhoneContactMergerThread = new Thread(new Runnable() { - @Override - public void run() { - Log.d(Config.LOGTAG,"start merging phone contacts with roster"); - for (Account account : accounts) { - account.getRoster().clearSystemAccounts(); - for (Bundle phoneContact : phoneContacts) { - if (Thread.interrupted()) { - Log.d(Config.LOGTAG,"interrupted merging phone contacts"); - return; - } - Jid jid; - try { - jid = Jid.fromString(phoneContact.getString("jid")); - } catch (final InvalidJidException e) { - continue; - } - final Contact contact = account.getRoster().getContact(jid); - String systemAccount = phoneContact.getInt("phoneid") - + "#" - + phoneContact.getString("lookup"); - contact.setSystemAccount(systemAccount); - contact.setPhotoUri(phoneContact.getString("photouri")); - getAvatarService().clear(contact); - contact.setSystemName(phoneContact.getString("displayname")); - } - } - Log.d(Config.LOGTAG,"finished merging phone contacts"); - updateAccountUi(); - } - }); - mPhoneContactMergerThread.start(); - } - - private void restoreFromDatabase() { - synchronized (this.conversations) { - final Map accountLookupTable = new Hashtable<>(); - for (Account account : this.accounts) { - accountLookupTable.put(account.getUuid(), account); - } - this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); - for (Conversation conversation : this.conversations) { - Account account = accountLookupTable.get(conversation.getAccountUuid()); - conversation.setAccount(account); - } - new Thread(new Runnable() { - @Override - public void run() { - Log.d(Config.LOGTAG,"restoring roster"); - for(Account account : accounts) { - databaseBackend.readRoster(account.getRoster()); - } - getBitmapCache().evictAll(); - Looper.prepare(); - PhoneHelper.loadPhoneContacts(getApplicationContext(), - new CopyOnWriteArrayList(), - XmppConnectionService.this); - Log.d(Config.LOGTAG,"restoring messages"); - for (Conversation conversation : conversations) { - conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); - checkDeletedFiles(conversation); - } - mRestoredFromDatabase = true; - Log.d(Config.LOGTAG,"restored all messages"); - updateConversationUi(); - } - }).start(); - } - } - - public List getConversations() { - return this.conversations; - } - - private void checkDeletedFiles(Conversation conversation) { - conversation.findMessagesWithFiles(new Conversation.OnMessageFound() { - - @Override - public void onMessageFound(Message message) { - if (!getFileBackend().isFileAvailable(message)) { - message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); - } - } - }); - } - - private void markFileDeleted(String uuid) { - for (Conversation conversation : getConversations()) { - Message message = conversation.findMessageWithFileAndUuid(uuid); - if (message != null) { - if (!getFileBackend().isFileAvailable(message)) { - message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); - updateConversationUi(); - } - return; - } - } - } - - public void populateWithOrderedConversations(final List list) { - populateWithOrderedConversations(list, true); - } - - public void populateWithOrderedConversations(final 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 void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { - Log.d(Config.LOGTAG,"load more messages for "+conversation.getName() + " prior to "+MessageGenerator.getTimestamp(timestamp)); - if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) { - return; - } - new Thread(new Runnable() { - @Override - public void run() { - final Account account = conversation.getAccount(); - List messages = databaseBackend.getMessages(conversation, 50,timestamp); - if (messages.size() > 0) { - conversation.addAll(0, messages); - checkDeletedFiles(conversation); - callback.onMoreMessagesLoaded(messages.size(), conversation); - } else if (conversation.hasMessagesLeftOnServer() - && account.isOnlineAndConnected() - && account.getXmppConnection().getFeatures().mam()) { - MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1); - if (query != null) { - query.setCallback(callback); - } - callback.informUser(R.string.fetching_history_from_server); - } - } - }).start(); - } - - public List getAccounts() { - return this.accounts; - } - - public Conversation find(final Iterable haystack, final Contact contact) { - for (final Conversation conversation : haystack) { - if (conversation.getContact() == contact) { - return conversation; - } - } - return null; - } - - public Conversation find(final Iterable haystack, final Account account, final Jid jid) { - if (jid == null) { - return null; - } - for (final Conversation conversation : haystack) { - if ((account == null || conversation.getAccount() == account) - && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) { - return conversation; - } - } - return null; - } - - public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) { - return this.findOrCreateConversation(account, jid, muc, null); - } - - public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) { - synchronized (this.conversations) { - 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); - conversation.setContactJid(jid); - } else { - conversation.setMode(Conversation.MODE_SINGLE); - conversation.setContactJid(jid.toBareJid()); - } - conversation.setNextEncryption(-1); - conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); - this.databaseBackend.updateConversation(conversation); - } else { - String conversationName; - Contact contact = account.getRoster().getContact(jid); - if (contact != null) { - conversationName = contact.getDisplayName(); - } else { - conversationName = jid.getLocalpart(); - } - if (muc) { - conversation = new Conversation(conversationName, account, jid, - Conversation.MODE_MULTI); - } else { - conversation = new Conversation(conversationName, account, jid.toBareJid(), - Conversation.MODE_SINGLE); - } - this.databaseBackend.createConversation(conversation); - } - if (account.getXmppConnection() != null - && account.getXmppConnection().getFeatures().mam() - && !muc) { - if (query == null) { - this.mMessageArchiveService.query(conversation); - } else { - if (query.getConversation() == null) { - this.mMessageArchiveService.query(conversation, query.getStart()); - } - } - } - checkDeletedFiles(conversation); - this.conversations.add(conversation); - updateConversationUi(); - return conversation; - } - } - - public void archiveConversation(Conversation conversation) { - conversation.setStatus(Conversation.STATUS_ARCHIVED); - conversation.setNextEncryption(-1); - synchronized (this.conversations) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getAccount().getStatus() == Account.State.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 createAccount(final Account account) { - account.initOtrEngine(this); - databaseBackend.createAccount(account); - this.accounts.add(account); - this.reconnectAccountInBackground(account); - updateAccountUi(); - } - - public void updateAccount(final Account account) { - this.statusListener.onStatusChanged(account); - databaseBackend.updateAccount(account); - reconnectAccount(account, false); - updateAccountUi(); - getNotificationService().updateErrorNotification(); - } - - public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { - final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); - sendIqPacket(account, iq, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setPassword(newPassword); - databaseBackend.updateAccount(account); - callback.onPasswordChangeSucceeded(); - } else { - callback.onPasswordChangeFailed(); - } - } - }); - } - - public void deleteAccount(final Account account) { - synchronized (this.conversations) { - for (final 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(); - getNotificationService().updateErrorNotification(); - } - } - - public void setOnConversationListChangedListener(OnConversationUpdate listener) { - synchronized (this) { - if (checkListeners()) { - switchToForeground(); - } - this.mOnConversationUpdate = listener; - this.mNotificationService.setIsInForeground(true); - if (this.convChangedListenerCount < 2) { - this.convChangedListenerCount++; - } - } - } - - public void removeOnConversationListChangedListener() { - synchronized (this) { - this.convChangedListenerCount--; - if (this.convChangedListenerCount <= 0) { - this.convChangedListenerCount = 0; - this.mOnConversationUpdate = null; - this.mNotificationService.setIsInForeground(false); - if (checkListeners()) { - switchToBackground(); - } - } - } - } - - public void setOnAccountListChangedListener(OnAccountUpdate listener) { - synchronized (this) { - if (checkListeners()) { - switchToForeground(); - } - this.mOnAccountUpdate = listener; - if (this.accountChangedListenerCount < 2) { - this.accountChangedListenerCount++; - } - } - } - - public void removeOnAccountListChangedListener() { - synchronized (this) { - this.accountChangedListenerCount--; - if (this.accountChangedListenerCount <= 0) { - this.mOnAccountUpdate = null; - this.accountChangedListenerCount = 0; - if (checkListeners()) { - switchToBackground(); - } - } - } - } - - public void setOnRosterUpdateListener(final OnRosterUpdate listener) { - synchronized (this) { - if (checkListeners()) { - switchToForeground(); - } - this.mOnRosterUpdate = listener; - if (this.rosterChangedListenerCount < 2) { - this.rosterChangedListenerCount++; - } - } - } - - public void removeOnRosterUpdateListener() { - synchronized (this) { - this.rosterChangedListenerCount--; - if (this.rosterChangedListenerCount <= 0) { - this.rosterChangedListenerCount = 0; - this.mOnRosterUpdate = null; - if (checkListeners()) { - switchToBackground(); - } - } - } - } - - public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { - synchronized (this) { - if (checkListeners()) { - switchToForeground(); - } - this.mOnUpdateBlocklist = listener; - if (this.updateBlocklistListenerCount < 2) { - this.updateBlocklistListenerCount++; - } - } - } - - public void removeOnUpdateBlocklistListener() { - synchronized (this) { - this.updateBlocklistListenerCount--; - if (this.updateBlocklistListenerCount <= 0) { - this.updateBlocklistListenerCount = 0; - this.mOnUpdateBlocklist = null; - if (checkListeners()) { - switchToBackground(); - } - } - } - } - - public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { - synchronized (this) { - if (checkListeners()) { - switchToForeground(); - } - this.mOnMucRosterUpdate = listener; - if (this.mucRosterChangedListenerCount < 2) { - this.mucRosterChangedListenerCount++; - } - } - } - - public void removeOnMucRosterUpdateListener() { - synchronized (this) { - this.mucRosterChangedListenerCount--; - if (this.mucRosterChangedListenerCount <= 0) { - this.mucRosterChangedListenerCount = 0; - this.mOnMucRosterUpdate = null; - if (checkListeners()) { - switchToBackground(); - } - } - } - } - - private boolean checkListeners() { - return (this.mOnAccountUpdate == null - && this.mOnConversationUpdate == null - && this.mOnRosterUpdate == null - && this.mOnUpdateBlocklist == null); - } - - private void switchToForeground() { - for (Account account : getAccounts()) { - if (account.getStatus() == Account.State.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.State.ONLINE) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().csi()) { - connection.sendInactive(); - } - } - } - for(Conversation conversation : getConversations()) { - conversation.setIncomingChatState(ChatState.ACTIVE); - } - this.mNotificationService.setIsInForeground(false); - Log.d(Config.LOGTAG, "app switched into background"); - } - - private void connectMultiModeConversations(Account account) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if ((conversation.getMode() == Conversation.MODE_MULTI) - && (conversation.getAccount() == account)) { - conversation.resetMucOptions(); - joinMuc(conversation); - } - } - } - - public void joinMuc(Conversation conversation) { - Account account = conversation.getAccount(); - account.pendingConferenceJoins.remove(conversation); - account.pendingConferenceLeaves.remove(conversation); - if (account.getStatus() == Account.State.ONLINE) { - final String nick = conversation.getMucOptions().getProposedNick(); - final Jid joinJid = conversation.getMucOptions().createJoinJid(nick); - if (joinJid == null) { - return; //safety net - } - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString()); - PresencePacket packet = new PresencePacket(); - packet.setFrom(conversation.getAccount().getJid()); - packet.setTo(joinJid); - Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); - if (conversation.getMucOptions().getPassword() != null) { - x.addChild("password").setContent(conversation.getMucOptions().getPassword()); - } - x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted())); - String sig = account.getPgpSignature(); - if (sig != null) { - packet.addChild("status").setContent("online"); - packet.addChild("x", "jabber:x:signed").setContent(sig); - } - sendPresencePacket(account, packet); - fetchConferenceConfiguration(conversation); - if (!joinJid.equals(conversation.getJid())) { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - } - conversation.setHasMessagesLeftOnServer(false); - } else { - account.pendingConferenceJoins.add(conversation); - } - } - - 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 UiCallback callback) { - final MucOptions options = conversation.getMucOptions(); - final Jid joinJid = options.createJoinJid(nick); - if (options.online()) { - Account account = conversation.getAccount(); - options.setOnRenameListener(new OnRenameListener() { - - @Override - public void onSuccess() { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - bookmark.setNick(nick); - pushBookmarks(bookmark.getAccount()); - } - callback.success(conversation); - } - - @Override - public void onFailure() { - callback.error(R.string.nick_in_use, conversation); - } - }); - - PresencePacket packet = new PresencePacket(); - packet.setTo(joinJid); - packet.setFrom(conversation.getAccount().getJid()); - - 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(joinJid); - databaseBackend.updateConversation(conversation); - if (conversation.getAccount().getStatus() == Account.State.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.State.ONLINE) { - PresencePacket packet = new PresencePacket(); - packet.setTo(conversation.getJid()); - packet.setFrom(conversation.getAccount().getJid()); - packet.setAttribute("type", "unavailable"); - sendPresencePacket(conversation.getAccount(), packet); - conversation.getMucOptions().setOffline(); - conversation.deregisterWithBookmark(); - Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() - + ": leaving muc " + conversation.getJid()); - } else { - account.pendingConferenceLeaves.add(conversation); - } - } - - private String findConferenceServer(final Account account) { - String server; - if (account.getXmppConnection() != null) { - server = account.getXmppConnection().getMucServer(); - if (server != null) { - return server; - } - } - for (Account other : getAccounts()) { - if (other != account && other.getXmppConnection() != null) { - server = other.getXmppConnection().getMucServer(); - if (server != null) { - return server; - } - } - } - return null; - } - - public void createAdhocConference(final Account account, final Iterable jids, final UiCallback callback) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString()); - if (account.getStatus() == Account.State.ONLINE) { - try { - String server = findConferenceServer(account); - if (server == null) { - if (callback != null) { - callback.error(R.string.no_conference_server_found, null); - } - return; - } - String name = new BigInteger(75, getRNG()).toString(32); - Jid jid = Jid.fromParts(name, server, null); - final Conversation conversation = findOrCreateConversation(account, jid, true); - joinMuc(conversation); - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_membersonly", "1"); - options.putString("muc#roomconfig_publicroom", "0"); - options.putString("muc#roomconfig_whois", "anyone"); - pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() { - @Override - public void onPushSucceeded() { - for (Jid invite : jids) { - invite(conversation, invite); - } - if (callback != null) { - callback.success(conversation); - } - } - - @Override - public void onPushFailed() { - if (callback != null) { - callback.error(R.string.conference_creation_failed, conversation); - } - } - }); - - } catch (InvalidJidException e) { - if (callback != null) { - callback.error(R.string.conference_creation_failed, null); - } - } - } else { - if (callback != null) { - callback.error(R.string.not_connected_try_again, null); - } - } - } - - public void fetchConferenceConfiguration(final Conversation conversation) { - IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(conversation.getJid().toBareJid()); - request.query("http://jabber.org/protocol/disco#info"); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { - ArrayList features = new ArrayList<>(); - for (Element child : packet.query().getChildren()) { - if (child != null && child.getName().equals("feature")) { - String var = child.getAttribute("var"); - if (var != null) { - features.add(var); - } - } - } - conversation.getMucOptions().updateFeatures(features); - updateConversationUi(); - } - } - }); - } - - public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) { - IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(conversation.getJid().toBareJid()); - request.query("http://jabber.org/protocol/muc#owner"); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { - Data data = Data.parse(packet.query().findChild("x", "jabber:x:data")); - for (Field field : data.getFields()) { - if (options.containsKey(field.getName())) { - field.setValue(options.getString(field.getName())); - } - } - data.submit(); - IqPacket set = new IqPacket(IqPacket.TYPE.SET); - set.setTo(conversation.getJid().toBareJid()); - set.query("http://jabber.org/protocol/muc#owner").addChild(data); - sendIqPacket(account, set, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - if (callback != null) { - callback.onPushSucceeded(); - } - } else { - if (callback != null) { - callback.onPushFailed(); - } - } - } - }); - } else { - if (callback != null) { - callback.onPushFailed(); - } - } - } - }); - } - - public void pushSubjectToConference(final Conversation conference, final String subject) { - MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject); - this.sendMessagePacket(conference.getAccount(), packet); - final MucOptions mucOptions = conference.getMucOptions(); - final MucOptions.User self = mucOptions.getSelf(); - if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - this.pushConferenceConfiguration(conference, options, null); - } - } - - public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { - final Jid jid = user.toBareJid(); - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); - sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - callback.onAffiliationChangedSuccessful(jid); - } else { - callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); - } - } - }); - } - - public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) { - List jids = new ArrayList<>(); - for (MucOptions.User user : conference.getMucOptions().getUsers()) { - if (user.getAffiliation() == before) { - jids.add(user.getJid()); - } - } - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString()); - sendIqPacket(conference.getAccount(), request, null); - } - - public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) { - IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - Log.d(Config.LOGTAG, request.toString()); - sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, packet.toString()); - if (packet.getType() == IqPacket.TYPE.RESULT) { - callback.onRoleChangedSuccessful(nick); - } else { - callback.onRoleChangeFailed(nick, R.string.could_not_change_role); - } - } - }); - } - - public void disconnect(Account account, boolean force) { - if ((account.getStatus() == Account.State.ONLINE) - || (account.getStatus() == Account.State.DISABLED)) { - if (!force) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getAccount() == account) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - leaveMuc(conversation); - } else { - if (conversation.endOtrIfNeeded()) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": ended otr session with " - + conversation.getJid()); - } - } - } - } - } - 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) { - final Account account = conversation.getAccount(); - final Session otrSession = conversation.getOtrSession(); - Log.d(Config.LOGTAG, - account.getJid().toBareJid() + " otr session established with " - + conversation.getJid() + "/" - + otrSession.getSessionID().getUserID()); - conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { - - @Override - public void onMessageFound(Message message) { - SessionID id = otrSession.getSessionID(); - try { - message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID())); - } catch (InvalidJidException e) { - return; - } - if (message.getType() == Message.TYPE_TEXT) { - MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true); - if (outPacket != null) { - message.setStatus(Message.STATUS_SEND); - databaseBackend.updateMessage(message); - sendMessagePacket(account, outPacket); - } - } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { - mJingleConnectionManager.createNewConnection(message); - } - updateConversationUi(); - } - }); - } - - public boolean renewSymmetricKey(Conversation conversation) { - Account account = conversation.getAccount(); - byte[] symmetricKey = new byte[32]; - this.mRandom.nextBytes(symmetricKey); - Session otrSession = conversation.getOtrSession(); - if (otrSession != null) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); - packet.setFrom(account.getJid()); - packet.addChild("private", "urn:xmpp:carbons:2"); - packet.addChild("no-copy", "urn:xmpp:hints"); - packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/" - + otrSession.getSessionID().getUserID()); - try { - packet.setBody(otrSession - .transformSending(CryptoHelper.FILETRANSFER - + CryptoHelper.bytesToHex(symmetricKey))[0]); - sendMessagePacket(account, packet); - conversation.setSymmetricKey(symmetricKey); - return true; - } catch (OtrException e) { - return false; - } - } - return false; - } - - public void pushContactToServer(final Contact contact) { - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.setOption(Contact.Options.DIRTY_PUSH); - final Account account = contact.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - final boolean ask = contact.getOption(Contact.Options.ASKING); - final boolean sendUpdates = contact - .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) - && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.query(Xmlns.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(final Account account, - final 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; - } - final 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) { - final 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().toBareJid() - + ": 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().toBareJid().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().toBareJid() - + ": 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().toBareJid(); - 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.State.ONLINE) { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - Element item = iq.query(Xmlns.ROSTER).addChild("item"); - item.setAttribute("jid", contact.getJid().toString()); - 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) { - synchronized (account) { - 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(Config.CONNECT_TIMEOUT, account.getUuid().hashCode()); - } else { - account.getRoster().clearPresences(); - account.setXmppConnection(null); - } - } - } - - public void reconnectAccountInBackground(final Account account) { - new Thread(new Runnable() { - @Override - public void run() { - reconnectAccount(account,false); - } - }).start(); - } - - public void invite(Conversation conversation, Jid contact) { - MessagePacket packet = mMessageGenerator.invite(conversation, contact); - sendMessagePacket(conversation.getAccount(), packet); - } - - public void resetSendingToWaiting(Account account) { - for (Conversation conversation : getConversations()) { - if (conversation.getAccount() == account) { - conversation.findUnsentTextMessages(new Conversation.OnMessageFound() { - - @Override - public void onMessageFound(Message message) { - markMessage(message, Message.STATUS_WAITING); - } - }); - } - } - } - - public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) { - if (uuid == null) { - return null; - } - for (Conversation conversation : getConversations()) { - if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) { - final Message message = conversation.findSentMessageWithUuid(uuid); - if (message != null) { - markMessage(message, status); - } - return message; - } - } - return null; - } - - public boolean markMessage(Conversation conversation, String uuid, - int status) { - if (uuid == null) { - return false; - } else { - Message message = conversation.findSentMessageWithUuid(uuid); - if (message != null) { - markMessage(message, status); - return true; - } else { - 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 sendChatStates() { - return getPreferences().getBoolean("chat_states", false); - } - - public boolean saveEncryptedMessages() { - return !getPreferences().getBoolean("dont_save_encrypted", false); - } - - public boolean indicateReceived() { - return getPreferences().getBoolean("indicate_received", false); - } - - public int unreadCount() { - int count = 0; - for(Conversation conversation : getConversations()) { - count += conversation.unreadCount(); - } - return count; - } - - 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 void updateBlocklistUi(final OnUpdateBlocklist.Status status) { - if (mOnUpdateBlocklist != null) { - mOnUpdateBlocklist.OnUpdateBlocklist(status); - } - } - - public void updateMucRosterUi() { - if (mOnMucRosterUpdate != null) { - mOnMucRosterUpdate.onMucRosterUpdate(); - } - } - - public Account findAccountByJid(final Jid accountJid) { - for (Account account : this.accounts) { - if (account.getJid().toBareJid().equals(accountJid.toBareJid())) { - 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(final Conversation conversation) { - mNotificationService.clear(conversation); - conversation.markRead(); - } - - public void sendReadMarker(final Conversation conversation) { - final Message markable = conversation.getLatestMarkableMessage(); - this.markRead(conversation); - if (Settings.CONFIRM_MESSAGE_READ && markable != null && markable.getRemoteMsgId() != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); - Account account = conversation.getAccount(); - final Jid to = markable.getCounterpart(); - MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId()); - this.sendMessagePacket(conversation.getAccount(), packet); - } - updateConversationUi(); - } - - 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 syncRosterToDisk(final Account account) { - new Thread(new Runnable() { - - @Override - public void run() { - databaseBackend.writeRoster(account.getRoster()); - } - }).start(); - - } - - public List getKnownHosts() { - final List hosts = new ArrayList<>(); - for (final Account account : getAccounts()) { - if (!hosts.contains(account.getServer().toString())) { - hosts.add(account.getServer().toString()); - } - for (final Contact contact : account.getRoster().getContacts()) { - if (contact.showInRoster()) { - final String server = contact.getServer().toString(); - if (server != null && !hosts.contains(server)) { - hosts.add(server); - } - } - } - } - return hosts; - } - - public List getKnownConferenceHosts() { - final ArrayList mucServers = new ArrayList<>(); - for (final Account account : accounts) { - if (account.getXmppConnection() != null) { - final 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(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendIqPacket(packet, callback); - } - } - - public void sendPresence(final Account account) { - sendPresencePacket(account, mPresenceGenerator.sendPresence(account)); - } - - public MessageGenerator getMessageGenerator() { - return this.mMessageGenerator; - } - - public PresenceGenerator getPresenceGenerator() { - return this.mPresenceGenerator; - } - - public IqGenerator getIqGenerator() { - return this.mIqGenerator; - } - - public IqParser getIqParser() { - return this.mIqParser; - } - - public JingleConnectionManager getJingleConnectionManager() { - return this.mJingleConnectionManager; - } - - public MessageArchiveService getMessageArchiveService() { - return this.mMessageArchiveService; - } - - public List findContacts(Jid 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; - } - - public void resendFailedMessages(final Message message) { - final Collection messages = new ArrayList<>(); - Message current = message; - while (current.getStatus() == Message.STATUS_SEND_FAILED) { - messages.add(current); - if (current.mergeable(current.next())) { - current = current.next(); - } else { - break; - } - } - for (final Message msg : messages) { - markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg); - } - } - - public void clearConversationHistory(final Conversation conversation) { - conversation.clearMessages(); - conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam - new Thread(new Runnable() { - @Override - public void run() { - databaseBackend.deleteMessagesInConversation(conversation); - } - }).start(); - } - - public void sendBlockRequest(final Blockable blockable) { - if (blockable != null && blockable.getBlockedJid() != null) { - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getBlocklist().add(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - } - } - }); - } - } - - public void sendUnblockRequest(final Blockable blockable) { - if (blockable != null && blockable.getJid() != null) { - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getBlocklist().remove(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - } - } - }); - } - } - - public interface OnMoreMessagesLoaded { - public void onMoreMessagesLoaded(int count, Conversation conversation); - - public void informUser(int r); - } - - public interface OnAccountPasswordChanged { - public void onPasswordChangeSucceeded(); - - public void onPasswordChangeFailed(); - } - - public interface OnAffiliationChanged { - public void onAffiliationChangedSuccessful(Jid jid); - - public void onAffiliationChangeFailed(Jid jid, int resId); - } - - public interface OnRoleChanged { - public void onRoleChangedSuccessful(String nick); - - public void onRoleChangeFailed(String nick, int resid); - } - - public interface OnConversationUpdate { - public void onConversationUpdate(); - } - - public interface OnAccountUpdate { - public void onAccountUpdate(); - } - - public interface OnRosterUpdate { - public void onRosterUpdate(); - } - - public interface OnMucRosterUpdate { - public void onMucRosterUpdate(); - } - - public interface OnConferenceOptionsPushed { - public void onPushSucceeded(); - - public void onPushFailed(); - } - - public class XmppConnectionBinder extends Binder { - public XmppConnectionService getService() { - return XmppConnectionService.this; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java deleted file mode 100644 index a61b872a..00000000 --- a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java +++ /dev/null @@ -1,15 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.os.Bundle; - -import eu.siacs.conversations.R; - -public class AboutActivity extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_about); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java deleted file mode 100644 index a57e1b89..00000000 --- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java +++ /dev/null @@ -1,33 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.preference.Preference; -import android.util.AttributeSet; - -import eu.siacs.conversations.utils.PhoneHelper; - -public class AboutPreference extends Preference { - public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - setSummary(); - } - - public AboutPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - setSummary(); - } - - @Override - protected void onClick() { - super.onClick(); - final Intent intent = new Intent(getContext(), AboutActivity.class); - getContext().startActivity(intent); - } - - private void setSummary() { - setSummary("Conversations " + PhoneHelper.getVersionName(getContext())); - } -} - diff --git a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java deleted file mode 100644 index 1a9fc95c..00000000 --- a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java +++ /dev/null @@ -1,124 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -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.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListView; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.ui.adapter.ListItemAdapter; - -public abstract class AbstractSearchableListItemActivity extends XmppActivity { - private ListView mListView; - private final List listItems = new ArrayList<>(); - private ArrayAdapter mListItemsAdapter; - - private EditText mSearchEditText; - - private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { - - @Override - public boolean onMenuItemActionExpand(final MenuItem item) { - mSearchEditText.post(new Runnable() { - - @Override - public void run() { - mSearchEditText.requestFocus(); - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mSearchEditText, - InputMethodManager.SHOW_IMPLICIT); - } - }); - - return true; - } - - @Override - public boolean onMenuItemActionCollapse(final MenuItem item) { - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), - InputMethodManager.HIDE_IMPLICIT_ONLY); - mSearchEditText.setText(""); - filterContacts(); - return true; - } - }; - - private final TextWatcher mSearchTextWatcher = new TextWatcher() { - - @Override - public void afterTextChanged(final Editable editable) { - filterContacts(editable.toString()); - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, - final int after) { - } - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, - final int count) { - } - }; - - public ListView getListView() { - return mListView; - } - - public List getListItems() { - return listItems; - } - - public EditText getSearchEditText() { - return mSearchEditText; - } - - public ArrayAdapter getListItemAdapter() { - return mListItemsAdapter; - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_choose_contact); - mListView = (ListView) findViewById(R.id.choose_contact_list); - mListView.setFastScrollEnabled(true); - mListItemsAdapter = new ListItemAdapter(this, listItems); - mListView.setAdapter(mListItemsAdapter); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.choose_contact, menu); - final MenuItem menuSearchView = menu.findItem(R.id.action_search); - final View mSearchView = menuSearchView.getActionView(); - mSearchEditText = (EditText) mSearchView - .findViewById(R.id.search_field); - mSearchEditText.addTextChangedListener(mSearchTextWatcher); - menuSearchView.setOnActionExpandListener(mOnActionExpandListener); - return true; - } - - protected void filterContacts() { - filterContacts(null); - } - - protected abstract void filterContacts(final String needle); - - @Override - void onBackendConnected() { - filterContacts(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java deleted file mode 100644 index 9cf7e9f8..00000000 --- a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java +++ /dev/null @@ -1,41 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Blockable; -import eu.siacs.conversations.services.XmppConnectionService; - -public final class BlockContactDialog { - public static void show(final Context context, - final XmppConnectionService xmppConnectionService, - final Blockable blockable) { - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - final boolean isBlocked = blockable.isBlocked(); - builder.setNegativeButton(R.string.cancel, null); - - if (blockable.getJid().isDomainJid() || blockable.getAccount().isBlocked(blockable.getJid().toDomainJid())) { - builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain); - builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, - blockable.getJid().toDomainJid())); - } else { - builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact); - builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text, - blockable.getJid().toBareJid())); - } - builder.setPositiveButton(isBlocked ? R.string.unblock : R.string.block, new DialogInterface.OnClickListener() { - - @Override - public void onClick(final DialogInterface dialog, final int which) { - if (isBlocked) { - xmppConnectionService.sendUnblockRequest(blockable); - } else { - xmppConnectionService.sendBlockRequest(blockable); - } - } - }); - builder.create().show(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java deleted file mode 100644 index 13d7f4fc..00000000 --- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java +++ /dev/null @@ -1,75 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.os.Bundle; -import android.text.Editable; -import android.view.View; -import android.widget.AdapterView; - -import java.util.Collections; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class BlocklistActivity extends AbstractSearchableListItemActivity implements OnUpdateBlocklist { - - private Account account = null; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - - @Override - public boolean onItemLongClick(final AdapterView parent, - final View view, - final int position, - final long id) { - BlockContactDialog.show(parent.getContext(), xmppConnectionService,(Contact) getListItems().get(position)); - return true; - } - }); - } - - @Override - public void onBackendConnected() { - for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getJid().toString().equals(getIntent().getStringExtra("account"))) { - this.account = account; - break; - } - } - filterContacts(); - } - - @Override - protected void filterContacts(final String needle) { - getListItems().clear(); - if (account != null) { - for (final Jid jid : account.getBlocklist()) { - final Contact contact = account.getRoster().getContact(jid); - if (contact.match(needle) && contact.isBlocked()) { - getListItems().add(contact); - } - } - Collections.sort(getListItems()); - } - runOnUiThread(new Runnable() { - @Override - public void run() { - getListItemAdapter().notifyDataSetChanged(); - } - }); - } - - @Override - public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { - final Editable editable = getSearchEditText().getText(); - if (editable != null) { - filterContacts(editable.toString()); - } else { - filterContacts(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java deleted file mode 100644 index aac435fd..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java +++ /dev/null @@ -1,110 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.os.Bundle; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -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; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged { - - private Button mChangePasswordButton; - private View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mAccount != null) { - final String currentPassword = mCurrentPassword.getText().toString(); - final String newPassword = mNewPassword.getText().toString(); - final String newPasswordConfirm = mNewPasswordConfirm.getText().toString(); - if (!currentPassword.equals(mAccount.getPassword())) { - mCurrentPassword.requestFocus(); - mCurrentPassword.setError(getString(R.string.account_status_unauthorized)); - } else if (!newPassword.equals(newPasswordConfirm)) { - mNewPasswordConfirm.requestFocus(); - mNewPasswordConfirm.setError(getString(R.string.passwords_do_not_match)); - } else if (newPassword.isEmpty()) { - mNewPassword.requestFocus(); - mNewPassword.setError(getString(R.string.password_should_not_be_empty)); - } else if (newPassword.trim().isEmpty()) { - mNewPassword.requestFocus(); - mNewPassword.setError(getString(R.string.password_should_not_contain_only_spaces)); - } else { - mCurrentPassword.setError(null); - mNewPassword.setError(null); - mNewPasswordConfirm.setError(null); - xmppConnectionService.updateAccountPasswordOnServer(mAccount, newPassword, ChangePasswordActivity.this); - mChangePasswordButton.setEnabled(false); - mChangePasswordButton.setTextColor(getSecondaryTextColor()); - mChangePasswordButton.setText(R.string.updating); - } - } - } - }; - private EditText mCurrentPassword; - private EditText mNewPassword; - private EditText mNewPasswordConfirm; - private Account mAccount; - - @Override - void onBackendConnected() { - try { - final String jid = getIntent() == null ? null : getIntent().getStringExtra("account"); - if (jid != null) { - this.mAccount = xmppConnectionService.findAccountByJid(Jid.fromString(jid)); - } - } catch (final InvalidJidException ignored) { - - } - - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_change_password); - Button mCancelButton = (Button) findViewById(R.id.left_button); - mCancelButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); - this.mChangePasswordButton = (Button) findViewById(R.id.right_button); - this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked); - this.mCurrentPassword = (EditText) findViewById(R.id.current_password); - this.mNewPassword = (EditText) findViewById(R.id.new_password); - this.mNewPasswordConfirm = (EditText) findViewById(R.id.new_password_confirm); - } - - @Override - public void onPasswordChangeSucceeded() { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(ChangePasswordActivity.this,R.string.password_changed,Toast.LENGTH_LONG).show(); - finish(); - } - }); - } - - @Override - public void onPasswordChangeFailed() { - runOnUiThread(new Runnable() { - @Override - public void run() { - mNewPassword.setError(getString(R.string.could_not_change_password)); - mChangePasswordButton.setEnabled(true); - mChangePasswordButton.setTextColor(getPrimaryTextColor()); - mChangePasswordButton.setText(R.string.change_password); - } - }); - - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java deleted file mode 100644 index c9e99ce5..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ /dev/null @@ -1,152 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.AbsListView.MultiChoiceModeListener; -import android.widget.AdapterView; -import android.widget.ListView; - -import java.util.Set; -import java.util.HashSet; -import java.util.Collections; -import java.util.List; -import java.util.ArrayList; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.ListItem; - -public class ChooseContactActivity extends AbstractSearchableListItemActivity { - - private Set selected; - private Set filterContacts; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - filterContacts = new HashSet<>(); - String[] contacts = getIntent().getStringArrayExtra("filter_contacts"); - if (contacts != null) { - Collections.addAll(filterContacts, contacts); - } - - if (getIntent().getBooleanExtra("multiple", false)) { - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); - getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() { - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), - InputMethodManager.HIDE_IMPLICIT_ONLY); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.select_multiple, menu); - selected = new HashSet(); - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch(item.getItemId()) { - case R.id.selection_submit: - final Intent request = getIntent(); - final Intent data = new Intent(); - data.putExtra("conversation", - request.getStringExtra("conversation")); - String[] selection = getSelectedContactJids(); - data.putExtra("contacts", selection); - data.putExtra("multiple", true); - setResult(RESULT_OK, data); - finish(); - return true; - } - return false; - } - - @Override - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { - Contact item = (Contact) getListItems().get(position); - if (checked) { - selected.add(item); - } else { - selected.remove(item); - } - int numSelected = selected.size(); - MenuItem selectButton = mode.getMenu().findItem(R.id.selection_submit); - String buttonText = getResources().getQuantityString(R.plurals.select_contact, - numSelected, numSelected); - selectButton.setTitle(buttonText); - } - }); - } - - getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { - - @Override - public void onItemClick(final AdapterView parent, final View view, - final int position, final long id) { - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), - InputMethodManager.HIDE_IMPLICIT_ONLY); - final Intent request = getIntent(); - final Intent data = new Intent(); - final ListItem mListItem = getListItems().get(position); - data.putExtra("contact", mListItem.getJid().toString()); - String account = request.getStringExtra("account"); - if (account == null && mListItem instanceof Contact) { - account = ((Contact) mListItem).getAccount().getJid().toBareJid().toString(); - } - data.putExtra("account", account); - data.putExtra("conversation", - request.getStringExtra("conversation")); - data.putExtra("multiple", false); - setResult(RESULT_OK, data); - finish(); - } - }); - - } - - protected void filterContacts(final String needle) { - getListItems().clear(); - for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - for (final Contact contact : account.getRoster().getContacts()) { - if (contact.showInRoster() && - !filterContacts.contains(contact.getJid().toBareJid().toString()) - && contact.match(needle)) { - getListItems().add(contact); - } - } - } - } - Collections.sort(getListItems()); - getListItemAdapter().notifyDataSetChanged(); - } - - private String[] getSelectedContactJids() { - List result = new ArrayList<>(); - for (Contact contact : selected) { - result.add(contact.getJid().toString()); - } - return result.toArray(new String[result.size()]); - } - -} diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java deleted file mode 100644 index e4bfd6ff..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ /dev/null @@ -1,563 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.app.PendingIntent; -import android.content.Context; -import android.content.DialogInterface; -import android.content.IntentSender.SendIntentException; -import android.graphics.Bitmap; -import android.os.Build; -import android.os.Bundle; -import android.view.ContextMenu; -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; - -import org.openintents.openpgp.util.OpenPgpUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -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.MucOptions; -import eu.siacs.conversations.entities.MucOptions.User; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; -import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed { - public static final String ACTION_VIEW_MUC = "view_muc"; - private Conversation mConversation; - private OnClickListener inviteListener = new OnClickListener() { - - @Override - public void onClick(View v) { - inviteToConversation(mConversation); - } - }; - 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 TextView mConferenceType; - private ImageButton mChangeConferenceSettingsButton; - private Button mInviteButton; - private String uuid = null; - private User mSelectedUser = null; - - private boolean mAdvancedMode = false; - - private UiCallback renameCallback = new UiCallback() { - @Override - public void success(Conversation object) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(ConferenceDetailsActivity.this,getString(R.string.your_nick_has_been_changed),Toast.LENGTH_SHORT).show(); - updateView(); - } - }); - - } - - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(ConferenceDetailsActivity.this,getString(errorCode),Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void userInputRequried(PendingIntent pi, Conversation object) { - - } - }; - private OnClickListener mChangeConferenceSettings = new OnClickListener() { - @Override - public void onClick(View v) { - final MucOptions mucOptions = mConversation.getMucOptions(); - AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this); - builder.setTitle(R.string.conference_options); - String[] options = {getString(R.string.members_only), - getString(R.string.non_anonymous)}; - final boolean[] values = new boolean[options.length]; - values[0] = mucOptions.membersOnly(); - values[1] = mucOptions.nonanonymous(); - builder.setMultiChoiceItems(options,values,new DialogInterface.OnMultiChoiceClickListener() { - @Override - public void onClick(DialogInterface dialog, int which, boolean isChecked) { - values[which] = isChecked; - } - }); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm,new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (!mucOptions.membersOnly() && values[0]) { - xmppConnectionService.changeAffiliationsInConference(mConversation, - MucOptions.Affiliation.NONE, - MucOptions.Affiliation.MEMBER); - } - Bundle options = new Bundle(); - options.putString("muc#roomconfig_membersonly", values[0] ? "1" : "0"); - options.putString("muc#roomconfig_whois", values[1] ? "anyone" : "moderators"); - options.putString("muc#roomconfig_persistentroom", "1"); - xmppConnectionService.pushConferenceConfiguration(mConversation, - options, - ConferenceDetailsActivity.this); - } - }); - builder.create().show(); - } - }; - private OnValueEdited onSubjectEdited = new OnValueEdited() { - - @Override - public void onValueEdited(String value) { - xmppConnectionService.pushSubjectToConference(mConversation,value); - } - }; - - @Override - public void onConversationUpdate() { - refreshUi(); - } - - @Override - public void onMucRosterUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - updateView(); - } - - @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); - mChangeConferenceSettingsButton = (ImageButton) findViewById(R.id.change_conference_button); - mChangeConferenceSettingsButton.setOnClickListener(this.mChangeConferenceSettings); - mConferenceType = (TextView) findViewById(R.id.muc_conference_type); - mInviteButton = (Button) findViewById(R.id.invite); - mInviteButton.setOnClickListener(inviteListener); - mConferenceType = (TextView) findViewById(R.id.muc_conference_type); - if (getActionBar() != null) { - getActionBar().setHomeButtonEnabled(true); - getActionBar().setDisplayHomeAsUpEnabled(true); - } - mEditNickButton.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - quickEdit(mConversation.getMucOptions().getActualNick(), - new OnValueEdited() { - - @Override - public void onValueEdited(String value) { - xmppConnectionService.renameInMuc(mConversation,value,renameCallback); - } - }); - } - }); - } - - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - switch (menuItem.getItemId()) { - case android.R.id.home: - finish(); - break; - case R.id.action_edit_subject: - if (mConversation != null) { - quickEdit(mConversation.getName(),this.onSubjectEdited); - } - break; - case R.id.action_save_as_bookmark: - saveAsBookmark(); - break; - case R.id.action_delete_bookmark: - deleteBookmark(); - break; - case R.id.action_advanced_mode: - this.mAdvancedMode = !menuItem.isChecked(); - menuItem.setChecked(this.mAdvancedMode); - invalidateOptionsMenu(); - updateView(); - break; - } - return super.onOptionsItemSelected(menuItem); - } - - @Override - protected String getShareableUri() { - if (mConversation != null) { - return "xmpp:" + mConversation.getJid().toBareJid().toString() + "?join"; - } else { - return ""; - } - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark); - MenuItem menuItemDeleteBookmark = menu.findItem(R.id.action_delete_bookmark); - MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode); - menuItemAdvancedMode.setChecked(mAdvancedMode); - Account account = mConversation.getAccount(); - if (account.hasBookmarkFor(mConversation.getJid().toBareJid())) { - menuItemSaveBookmark.setVisible(false); - menuItemDeleteBookmark.setVisible(true); - } else { - menuItemDeleteBookmark.setVisible(false); - menuItemSaveBookmark.setVisible(true); - } - return true; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.muc_details, menu); - return true; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - Object tag = v.getTag(); - if (tag instanceof User) { - getMenuInflater().inflate(R.menu.muc_details_context,menu); - final User user = (User) tag; - final User self = mConversation.getMucOptions().getSelf(); - this.mSelectedUser = user; - String name; - if (user.getJid() != null) { - final Contact contact = user.getContact(); - if (contact != null) { - name = contact.getDisplayName(); - } else { - name = user.getJid().toBareJid().toString(); - } - menu.setHeaderTitle(name); - MenuItem startConversation = menu.findItem(R.id.start_conversation); - MenuItem giveMembership = menu.findItem(R.id.give_membership); - MenuItem removeMembership = menu.findItem(R.id.remove_membership); - MenuItem giveAdminPrivileges = menu.findItem(R.id.give_admin_privileges); - MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges); - MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room); - MenuItem banFromConference = menu.findItem(R.id.ban_from_conference); - startConversation.setVisible(true); - if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && - self.getAffiliation().outranks(user.getAffiliation())) { - if (mAdvancedMode) { - if (user.getAffiliation() == MucOptions.Affiliation.NONE) { - giveMembership.setVisible(true); - } else { - removeMembership.setVisible(true); - } - banFromConference.setVisible(true); - } else { - removeFromRoom.setVisible(true); - } - if (user.getAffiliation() != MucOptions.Affiliation.ADMIN) { - giveAdminPrivileges.setVisible(true); - } else { - removeAdminPrivileges.setVisible(true); - } - } - } - - } - super.onCreateContextMenu(menu,v,menuInfo); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.start_conversation: - startConversation(mSelectedUser); - return true; - case R.id.give_admin_privileges: - xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.ADMIN,this); - return true; - case R.id.give_membership: - xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this); - return true; - case R.id.remove_membership: - xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.NONE,this); - return true; - case R.id.remove_admin_privileges: - xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this); - return true; - case R.id.remove_from_room: - removeFromRoom(mSelectedUser); - return true; - case R.id.ban_from_conference: - xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this); - xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this); - return true; - default: - return super.onContextItemSelected(item); - } - } - - private void removeFromRoom(final User user) { - if (mConversation.getMucOptions().membersOnly()) { - xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.NONE,this); - xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.ban_from_conference); - builder.setMessage(getString(R.string.removing_from_public_conference,user.getName())); - builder.setNegativeButton(R.string.cancel,null); - builder.setPositiveButton(R.string.ban_now,new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this); - xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this); - } - }); - builder.create().show(); - } - } - - protected void startConversation(User user) { - if (user.getJid() != null) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getJid().toBareJid(),false); - switchToConversation(conversation); - } - } - - protected void saveAsBookmark() { - Account account = mConversation.getAccount(); - Bookmark bookmark = new Bookmark(account, mConversation.getJid().toBareJid()); - if (!mConversation.getJid().isBareJid()) { - bookmark.setNick(mConversation.getJid().getResourcepart()); - } - bookmark.setAutojoin(true); - account.getBookmarks().add(bookmark); - xmppConnectionService.pushBookmarks(account); - mConversation.setBookmark(bookmark); - } - - protected void deleteBookmark() { - Account account = mConversation.getAccount(); - Bookmark bookmark = mConversation.getBookmark(); - bookmark.unregisterConversation(); - account.getBookmarks().remove(bookmark); - xmppConnectionService.pushBookmarks(account); - } - - @Override - void onBackendConnected() { - if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { - this.uuid = getIntent().getExtras().getString("uuid"); - } - if (uuid != null) { - this.mConversation = xmppConnectionService - .findConversationByUuid(uuid); - if (this.mConversation != null) { - updateView(); - } - } - } - - private void updateView() { - final MucOptions mucOptions = mConversation.getMucOptions(); - final User self = mucOptions.getSelf(); - mAccountJid.setText(getString(R.string.using_account, mConversation - .getAccount().getJid().toBareJid())); - mYourPhoto.setImageBitmap(avatarService().get(mConversation.getAccount(), getPixel(48))); - setTitle(mConversation.getName()); - mFullJid.setText(mConversation.getJid().toBareJid().toString()); - mYourNick.setText(mucOptions.getActualNick()); - mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); - if (mucOptions.online()) { - mMoreDetails.setVisibility(View.VISIBLE); - final String status = getStatus(self); - if (status != null) { - mRoleAffiliaton.setVisibility(View.VISIBLE); - mRoleAffiliaton.setText(status); - } else { - mRoleAffiliaton.setVisibility(View.GONE); - } - if (mucOptions.membersOnly()) { - mConferenceType.setText(R.string.private_conference); - } else { - mConferenceType.setText(R.string.public_conference); - } - if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - mChangeConferenceSettingsButton.setVisibility(View.VISIBLE); - } else { - mChangeConferenceSettingsButton.setVisibility(View.GONE); - } - } - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - membersView.removeAllViews(); - final ArrayList users = new ArrayList<>(); - users.addAll(mConversation.getMucOptions().getUsers()); - Collections.sort(users,new Comparator() { - @Override - public int compare(User lhs, User rhs) { - return lhs.getName().compareToIgnoreCase(rhs.getName()); - } - }); - for (final User user : users) { - View view = inflater.inflate(R.layout.contact, membersView,false); - this.setListItemBackgroundOnView(view); - view.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - highlightInMuc(mConversation, user.getName()); - } - }); - registerForContextMenu(view); - view.setTag(user); - TextView tvDisplayName = (TextView) view.findViewById(R.id.contact_display_name); - TextView tvKey = (TextView) view.findViewById(R.id.key); - TextView tvStatus = (TextView) view.findViewById(R.id.contact_jid); - if (mAdvancedMode && user.getPgpKeyId() != 0) { - tvKey.setVisibility(View.VISIBLE); - tvKey.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - viewPgpKey(user); - } - }); - tvKey.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); - } - Bitmap bm; - Contact contact = user.getContact(); - if (contact != null) { - bm = avatarService().get(contact, getPixel(48)); - tvDisplayName.setText(contact.getDisplayName()); - tvStatus.setText(user.getName() + " \u2022 " + getStatus(user)); - } else { - bm = avatarService().get(user.getName(), getPixel(48)); - tvDisplayName.setText(user.getName()); - tvStatus.setText(getStatus(user)); - - } - ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); - iv.setImageBitmap(bm); - membersView.addView(view); - if (mConversation.getMucOptions().canInvite()) { - mInviteButton.setVisibility(View.VISIBLE); - } else { - mInviteButton.setVisibility(View.GONE); - } - } - } - - private String getStatus(User user) { - if (mAdvancedMode) { - StringBuilder builder = new StringBuilder(); - builder.append(getString(user.getAffiliation().getResId())); - builder.append(" ("); - builder.append(getString(user.getRole().getResId())); - builder.append(')'); - return builder.toString(); - } else { - return getString(user.getAffiliation().getResId()); - } - } - - @SuppressWarnings("deprecation") - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private void setListItemBackgroundOnView(View view) { - int sdk = android.os.Build.VERSION.SDK_INT; - if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { - view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground)); - } else { - view.setBackground(getResources().getDrawable(R.drawable.greybackground)); - } - } - - private void viewPgpKey(User user) { - PgpEngine pgp = xmppConnectionService.getPgpEngine(); - if (pgp != null) { - PendingIntent intent = pgp.getIntentForKey( - mConversation.getAccount(), user.getPgpKeyId()); - if (intent != null) { - try { - startIntentSenderForResult(intent.getIntentSender(), 0, - null, 0, 0, 0); - } catch (SendIntentException ignored) { - - } - } - } - } - - @Override - public void onAffiliationChangedSuccessful(Jid jid) { - - } - - @Override - public void onAffiliationChangeFailed(Jid jid, int resId) { - displayToast(getString(resId,jid.toBareJid().toString())); - } - - @Override - public void onRoleChangedSuccessful(String nick) { - - } - - @Override - public void onRoleChangeFailed(String nick, int resId) { - displayToast(getString(resId,nick)); - } - - @Override - public void onPushSucceeded() { - displayToast(getString(R.string.modified_conference_options)); - } - - @Override - public void onPushFailed() { - displayToast(getString(R.string.could_not_modify_conference_options)); - } - - private void displayToast(final String msg) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(ConferenceDetailsActivity.this,msg,Toast.LENGTH_SHORT).show(); - } - }); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java deleted file mode 100644 index 40a4587c..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ /dev/null @@ -1,471 +0,0 @@ -package eu.siacs.conversations.ui; - -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.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.preference.PreferenceManager; -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.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.QuickContactBadge; -import android.widget.TextView; - -import org.openintents.openpgp.util.OpenPgpUtils; - -import java.util.List; - -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.ListItem; -import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; -import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist { - public static final String ACTION_VIEW_CONTACT = "view_contact"; - - private Contact contact; - private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - xmppConnectionService.deleteContactOnServer(contact); - } - }; - 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 Jid accountJid; - private Jid contactJid; - private TextView contactJidTv; - private TextView accountJidTv; - private TextView lastseen; - private CheckBox send; - private CheckBox receive; - private Button addContactButton; - private QuickContactBadge badge; - private LinearLayout keys; - private LinearLayout tags; - private boolean showDynamicTags; - - 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().toString()); - 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(); - } - }; - - @Override - public void onRosterUpdate() { - refreshUi(); - } - - @Override - public void onAccountUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - invalidateOptionsMenu(); - populateView(); - } - - @Override - protected String getShareableUri() { - if (contact != null) { - return contact.getShareableUri(); - } else { - return ""; - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { - try { - this.accountJid = Jid.fromString(getIntent().getExtras().getString("account")); - } catch (final InvalidJidException ignored) { - } - try { - this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact")); - } catch (final InvalidJidException ignored) { - } - } - setContentView(R.layout.activity_contact_details); - - contactJidTv = (TextView) findViewById(R.id.details_contactjid); - accountJidTv = (TextView) findViewById(R.id.details_account); - 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); - addContactButton = (Button) findViewById(R.id.add_contact_button); - addContactButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - showAddToRosterDialog(contact); - } - }); - keys = (LinearLayout) findViewById(R.id.details_contact_keys); - tags = (LinearLayout) findViewById(R.id.tags); - if (getActionBar() != null) { - getActionBar().setHomeButtonEnabled(true); - getActionBar().setDisplayHomeAsUpEnabled(true); - } - - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem menuItem) { - final 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; - case R.id.action_block: - BlockContactDialog.show(this, xmppConnectionService, contact); - break; - case R.id.action_unblock: - BlockContactDialog.show(this, xmppConnectionService, contact); - break; - } - return super.onOptionsItemSelected(menuItem); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.contact_details, menu); - MenuItem block = menu.findItem(R.id.action_block); - MenuItem unblock = menu.findItem(R.id.action_unblock); - MenuItem edit = menu.findItem(R.id.action_edit_contact); - MenuItem delete = menu.findItem(R.id.action_delete_contact); - final XmppConnection connection = contact.getAccount().getXmppConnection(); - if (connection != null && connection.getFeatures().blocking()) { - if (this.contact.isBlocked()) { - menu.findItem(R.id.action_block).setVisible(false); - } else { - menu.findItem(R.id.action_unblock).setVisible(false); - } - } else { - menu.findItem(R.id.action_unblock).setVisible(false); - menu.findItem(R.id.action_block).setVisible(false); - } - if (!contact.showInRoster()) { - edit.setVisible(false); - delete.setVisible(false); - } - return true; - } - - private void populateView() { - setTitle(contact.getDisplayName()); - if (contact.showInRoster()) { - send.setVisibility(View.VISIBLE); - receive.setVisibility(View.VISIBLE); - addContactButton.setVisibility(View.GONE); - send.setOnCheckedChangeListener(null); - receive.setOnCheckedChangeListener(null); - - 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().isOnlineAndConnected()) { - receive.setEnabled(true); - send.setEnabled(true); - } else { - receive.setEnabled(false); - send.setEnabled(false); - } - - send.setOnCheckedChangeListener(this.mOnSendCheckedChange); - receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange); - } else { - addContactButton.setVisibility(View.VISIBLE); - send.setVisibility(View.GONE); - receive.setVisibility(View.GONE); - } - - if (contact.isBlocked() && !this.showDynamicTags) { - lastseen.setText(R.string.contact_blocked); - } else { - lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.lastseen.time)); - } - - if (contact.getPresences().size() > 1) { - contactJidTv.setText(contact.getJid() + " (" - + contact.getPresences().size() + ")"); - } else { - contactJidTv.setText(contact.getJid().toString()); - } - accountJidTv.setText(getString(R.string.using_account, contact - .getAccount().getJid().toBareJid())); - prepareContactBadge(badge, contact); - if (contact.getSystemAccount() == null) { - badge.setOnClickListener(onBadgeClick); - } - - keys.removeAllViews(); - boolean hasKeys = false; - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - for(final String otrFingerprint : contact.getOtrFingerprints()) { - hasKeys = true; - 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(CryptoHelper.prettifyFingerprint(otrFingerprint)); - keys.addView(view); - remove.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - confirmToDeleteFingerprint(otrFingerprint); - } - }); - } - if (contact.getPgpKeyId() != 0) { - hasKeys = true; - 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); - } - - List tagList = contact.getTags(); - if (tagList.size() == 0 || !this.showDynamicTags) { - tags.setVisibility(View.GONE); - } else { - tags.setVisibility(View.VISIBLE); - tags.removeAllViewsInLayout(); - for(final ListItem.Tag tag : tagList) { - final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tags,false); - tv.setText(tag.getName()); - tv.setBackgroundColor(tag.getColor()); - tags.addView(tv); - } - } - } - - 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() { - if ((accountJid != null) && (contactJid != null)) { - Account account = xmppConnectionService - .findAccountByJid(accountJid); - if (account == null) { - return; - } - this.contact = account.getRoster().getContact(contactJid); - populateView(); - } - } - - @Override - public void OnUpdateBlocklist(final Status status) { - runOnUiThread(new Runnable() { - - @Override - public void run() { - invalidateOptionsMenu(); - populateView(); - } - }); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java deleted file mode 100644 index 0fbaa479..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ /dev/null @@ -1,1116 +0,0 @@ -package eu.siacs.conversations.ui; - -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.Intent; -import android.content.IntentSender.SendIntentException; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.MediaStore; -import android.support.v4.widget.SlidingPaneLayout; -import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; -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; - -import net.java.otr4j.session.SessionStatus; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Blockable; -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 eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import github.ankushsachdeva.emojicon.EmojiconEditText; - -public class ConversationActivity extends XmppActivity - implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist { - - public static final String ACTION_DOWNLOAD = "eu.siacs.conversations.action.DOWNLOAD"; - - public static final String VIEW_CONVERSATION = "viewConversation"; - public static final String CONVERSATION = "conversationUuid"; - public static final String MESSAGE = "messageUuid"; - public static final String TEXT = "text"; - public static final String NICK = "nick"; - - public static final int REQUEST_SEND_MESSAGE = 0x0201; - public static final int REQUEST_DECRYPT_PGP = 0x0202; - 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_CHOOSE_FILE = 0x0303; - private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; - private static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; - private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; - private static final String STATE_PANEL_OPEN = "state_panel_open"; - private static final String STATE_PENDING_URI = "state_pending_uri"; - - private String mOpenConverstaion = null; - private boolean mPanelOpen = true; - private Uri mPendingImageUri = null; - private Uri mPendingFileUri = null; - private Uri mPendingGeoUri = null; - - private View mContentView; - - private List conversationList = new ArrayList<>(); - private Conversation mSelectedConversation = null; - private ListView listView; - private ConversationFragment mConversationFragment; - - private ArrayAdapter listAdapter; - - private Toast prepareFileToast; - - private boolean mActivityPaused = false; - private boolean mRedirected = true; - - public Conversation getSelectedConversation() { - return this.mSelectedConversation; - } - - public void setSelectedConversation(Conversation conversation) { - this.mSelectedConversation = conversation; - } - - public void showConversationsOverview() { - if (mContentView instanceof SlidingPaneLayout) { - SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; - mSlidingPaneLayout.openPane(); - } - } - - @Override - protected String getShareableUri() { - Conversation conversation = getSelectedConversation(); - if (conversation != null) { - return conversation.getAccount().getShareableUri(); - } else { - return ""; - } - } - - 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(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mOpenConverstaion = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null); - mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true); - String pending = savedInstanceState.getString(STATE_PENDING_URI, null); - if (pending != null) { - mPendingImageUri = Uri.parse(pending); - } - } - - setContentView(R.layout.fragment_conversations_overview); - - this.mConversationFragment = new ConversationFragment(); - FragmentTransaction transaction = getFragmentManager().beginTransaction(); - transaction.replace(R.id.selected_conversation, this.mConversationFragment, "conversation"); - transaction.commit(); - - listView = (ListView) findViewById(R.id.list); - this.listAdapter = new ConversationAdapter(this, conversationList); - listView.setAdapter(this.listAdapter); - - if (getActionBar() != null) { - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setHomeButtonEnabled(false); - } - - listView.setOnItemClickListener(new OnItemClickListener() { - - @Override - public void onItemClick(AdapterView arg0, View clickedView, - int position, long arg3) { - if (getSelectedConversation() != conversationList.get(position)) { - setSelectedConversation(conversationList.get(position)); - ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation()); - } - hideConversationsOverview(); - openConversation(); - } - }); - 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) { - updateActionBarTitle(); - invalidateOptionsMenu(); - hideKeyboard(); - if (xmppConnectionServiceBound) { - xmppConnectionService.getNotificationService() - .setOpenConversation(null); - } - closeContextMenu(); - } - - @Override - public void onPanelClosed(View arg0) { - openConversation(); - } - - @Override - public void onPanelSlide(View arg0, float arg1) { - // TODO Auto-generated method stub - - } - }); - } - } - - @Override - public void switchToConversation(Conversation conversation) { - setSelectedConversation(conversation); - runOnUiThread(new Runnable() { - @Override - public void run() { - ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation()); - openConversation(); - } - }); - } - - private void updateActionBarTitle() { - updateActionBarTitle(isConversationsOverviewHideable() && !isConversationsOverviewVisable()); - } - - private void updateActionBarTitle(boolean titleShouldBeName) { - final ActionBar ab = getActionBar(); - final Conversation conversation = getSelectedConversation(); - if (ab != null) { - if (titleShouldBeName && conversation != null) { - ab.setDisplayHomeAsUpEnabled(true); - ab.setHomeButtonEnabled(true); - if (conversation.getMode() == Conversation.MODE_SINGLE || useSubjectToIdentifyConference()) { - ab.setTitle(conversation.getName()); - } else { - ab.setTitle(conversation.getJid().toBareJid().toString()); - } - } else { - ab.setDisplayHomeAsUpEnabled(false); - ab.setHomeButtonEnabled(false); - ab.setTitle(R.string.app_name); - } - } - } - - private void openConversation() { - this.updateActionBarTitle(); - this.invalidateOptionsMenu(); - if (xmppConnectionServiceBound) { - final Conversation conversation = getSelectedConversation(); - xmppConnectionService.getNotificationService().setOpenConversation(conversation); - sendReadMarkerIfNecessary(conversation); - } - listAdapter.notifyDataSetChanged(); - } - - public void sendReadMarkerIfNecessary(final Conversation conversation) { - if (!mActivityPaused && conversation != null) { - if (!conversation.isRead()) { - xmppConnectionService.sendReadMarker(conversation); - } else { - xmppConnectionService.markRead(conversation); - } - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.conversations, menu); - final MenuItem menuSecure = menu.findItem(R.id.action_security); - final MenuItem menuArchive = menu.findItem(R.id.action_archive); - final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details); - final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); - final MenuItem menuAttach = menu.findItem(R.id.action_attach_file); - final MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history); - final MenuItem menuAdd = menu.findItem(R.id.action_add); - final MenuItem menuInviteContact = menu.findItem(R.id.action_invite); - final MenuItem menuMute = menu.findItem(R.id.action_mute); - final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); - - 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); - menuUnmute.setVisible(false); - } else { - menuAdd.setVisible(!isConversationsOverviewHideable()); - if (this.getSelectedConversation() != null) { - if (this.getSelectedConversation().getLatestMessage() - .getEncryption() != Message.ENCRYPTION_NONE) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - menuSecure.setIcon(R.drawable.ic_lock_outline_white_48dp); - } else { - menuSecure.setIcon(R.drawable.ic_action_secure); - } - } - if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { - menuContactDetails.setVisible(false); - menuAttach.setVisible(false); - menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite()); - } else { - menuMucDetails.setVisible(false); - } - if (this.getSelectedConversation().isMuted()) { - menuMute.setVisible(false); - } else { - menuUnmute.setVisible(false); - } - } - } - return true; - } - - private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { - if (attachmentChoice == ATTACHMENT_CHOICE_LOCATION && encryption != Message.ENCRYPTION_OTR) { - getSelectedConversation().setNextCounterpart(null); - Intent intent = new Intent("eu.siacs.conversations.location.request"); - startActivityForResult(intent,attachmentChoice); - } else { - selectPresence(getSelectedConversation(), new OnPresenceSelected() { - - @Override - public void onPresenceSelected() { - Intent intent = new Intent(); - boolean chooser = false; - switch (attachmentChoice) { - case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - intent.setAction(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - chooser = true; - break; - case ATTACHMENT_CHOICE_TAKE_PHOTO: - mPendingImageUri = xmppConnectionService.getFileBackend().getTakePhotoUri(); - intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, mPendingImageUri); - break; - case ATTACHMENT_CHOICE_CHOOSE_FILE: - chooser = true; - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setAction(Intent.ACTION_GET_CONTENT); - break; - case ATTACHMENT_CHOICE_RECORD_VOICE: - intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION); - break; - case ATTACHMENT_CHOICE_LOCATION: - intent.setAction("eu.siacs.conversations.location.request"); - break; - } - if (intent.resolveActivity(getPackageManager()) != null) { - if (chooser) { - startActivityForResult( - Intent.createChooser(intent, getString(R.string.perform_action_with)), - attachmentChoice); - } else { - startActivityForResult(intent, attachmentChoice); - } - } - } - }); - } - } - - private void attachFile(final int attachmentChoice) { - final Conversation conversation = getSelectedConversation(); - final int encryption = conversation.getNextEncryption(forceEncryption()); - if (encryption == 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,encryption); - } - - @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,Message.ENCRYPTION_NONE); - } - }); - } - } - } else { - showInstallPgpDialog(); - } - } else { - selectPresenceToAttachFile(attachmentChoice,encryption); - } - } - - @Override - public boolean onOptionsItemSelected(final 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: - switchToContactDetails(getSelectedConversation().getContact()); - 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; - case R.id.action_unmute: - unmuteConversation(getSelectedConversation()); - break; - case R.id.action_block: - BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation()); - break; - case R.id.action_unblock: - BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation()); - break; - default: - break; - } - return super.onOptionsItemSelected(item); - } else { - return super.onOptionsItemSelected(item); - } - } - - public void endConversation(Conversation conversation) { - showConversationsOverview(); - xmppConnectionService.archiveConversation(conversation); - if (conversationList.size() > 0) { - setSelectedConversation(conversationList.get(0)); - this.mConversationFragment.reInit(getSelectedConversation()); - } 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); - } else { - updateConversationList(); - ConversationActivity.this.mConversationFragment.updateMessages(); - } - } - }); - 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); - if (new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null) { - attachFilePopup.getMenu().findItem(R.id.attach_record_voice).setVisible(false); - } - if (new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null) { - attachFilePopup.getMenu().findItem(R.id.attach_location).setVisible(false); - } - 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_choose_file: - attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE); - break; - case R.id.attach_record_voice: - attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); - break; - case R.id.attach_location: - attachFile(ATTACHMENT_CHOICE_LOCATION); - break; - } - return false; - } - }); - attachFilePopup.show(); - } - - public void verifyOtrSessionDialog(final Conversation conversation, View view) { - if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { - Toast.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show(); - return; - } - if (view == null) { - return; - } - PopupMenu popup = new PopupMenu(this, view); - popup.inflate(R.menu.verification_choices); - popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - Intent intent = new Intent(ConversationActivity.this, VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); - intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); - switch (menuItem.getItemId()) { - case R.id.scan_fingerprint: - intent.putExtra("mode",VerifyOTRActivity.MODE_SCAN_FINGERPRINT); - break; - case R.id.ask_question: - intent.putExtra("mode",VerifyOTRActivity.MODE_ASK_QUESTION); - break; - case R.id.manual_verification: - intent.putExtra("mode",VerifyOTRActivity.MODE_MANUAL_VERIFICATION); - break; - } - startActivity(intent); - return true; - } - }); - popup.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); - final int[] durations = getResources().getIntArray( - R.array.mute_options_durations); - builder.setItems(R.array.mute_options_descriptions, - new OnClickListener() { - - @Override - public void onClick(final DialogInterface dialog, final int which) { - final long till; - if (durations[which] == -1) { - till = Long.MAX_VALUE; - } else { - till = System.currentTimeMillis() + (durations[which] * 1000); - } - conversation.setMutedTill(till); - ConversationActivity.this.xmppConnectionService.databaseBackend - .updateConversation(conversation); - updateConversationList(); - ConversationActivity.this.mConversationFragment.updateMessages(); - invalidateOptionsMenu(); - } - }); - builder.create().show(); - } - - public void unmuteConversation(final Conversation conversation) { - conversation.setMutedTill(0); - this.xmppConnectionService.databaseBackend.updateConversation(conversation); - updateConversationList(); - ConversationActivity.this.mConversationFragment.updateMessages(); - invalidateOptionsMenu(); - } - - @Override - public void onBackPressed() { - if (!isConversationsOverviewVisable()) { - showConversationsOverview(); - } else { - moveTaskToBack(true); - } - } - - @Override - protected void onNewIntent(final Intent intent) { - if (xmppConnectionServiceBound) { - if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) { - handleViewConversationIntent(intent); - } - } else { - setIntent(intent); - } - } - - @Override - public void onStart() { - super.onStart(); - this.mRedirected = false; - if (this.xmppConnectionServiceBound) { - this.onBackendConnected(); - } - if (conversationList.size() >= 1) { - this.onConversationUpdate(); - } - } - - @Override - public void onPause() { - super.onPause(); - this.mActivityPaused = true; - if (this.xmppConnectionServiceBound) { - this.xmppConnectionService.getNotificationService().setIsInForeground(false); - } - } - - @Override - public void onResume() { - super.onResume(); - final int theme = findTheme(); - final boolean usingEnterKey = usingEnterKey(); - if (this.mTheme != theme || usingEnterKey != mUsingEnterKey) { - recreate(); - } - this.mActivityPaused = false; - if (this.xmppConnectionServiceBound) { - this.xmppConnectionService.getNotificationService().setIsInForeground(true); - } - - if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) { - sendReadMarkerIfNecessary(getSelectedConversation()); - } - - } - - @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { - Conversation conversation = getSelectedConversation(); - if (conversation != null) { - savedInstanceState.putString(STATE_OPEN_CONVERSATION, - conversation.getUuid()); - } - savedInstanceState.putBoolean(STATE_PANEL_OPEN, - isConversationsOverviewVisable()); - if (this.mPendingImageUri != null) { - savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUri.toString()); - } - super.onSaveInstanceState(savedInstanceState); - } - - @Override - void onBackendConnected() { - this.xmppConnectionService.getNotificationService().setIsInForeground(true); - updateConversationList(); - if (xmppConnectionService.getAccounts().size() == 0) { - if (!mRedirected) { - this.mRedirected = true; - startActivity(new Intent(this, EditAccountActivity.class)); - finish(); - } - } else if (conversationList.size() <= 0) { - if (!mRedirected) { - this.mRedirected = true; - Intent intent = new Intent(this, StartConversationActivity.class); - intent.putExtra("init",true); - startActivity(intent); - finish(); - } - } else if (getIntent() != null && VIEW_CONVERSATION.equals(getIntent().getType())) { - handleViewConversationIntent(getIntent()); - } else if (selectConversationByUuid(mOpenConverstaion)) { - if (mPanelOpen) { - showConversationsOverview(); - } else { - if (isConversationsOverviewHideable()) { - openConversation(); - } - } - this.mConversationFragment.reInit(getSelectedConversation()); - mOpenConverstaion = null; - } else if (getSelectedConversation() != null) { - this.mConversationFragment.reInit(getSelectedConversation()); - } else { - showConversationsOverview(); - mPendingImageUri = null; - mPendingFileUri = null; - mPendingGeoUri = null; - setSelectedConversation(conversationList.get(0)); - this.mConversationFragment.reInit(getSelectedConversation()); - } - - if (mPendingImageUri != null) { - attachImageToConversation(getSelectedConversation(),mPendingImageUri); - mPendingImageUri = null; - } else if (mPendingFileUri != null) { - attachFileToConversation(getSelectedConversation(),mPendingFileUri); - mPendingFileUri = null; - } else if (mPendingGeoUri != null) { - attachLocationToConversation(getSelectedConversation(),mPendingGeoUri); - mPendingGeoUri = null; - } - ExceptionHelper.checkForCrash(this, this.xmppConnectionService); - setIntent(new Intent()); - } - - private void handleViewConversationIntent(final Intent intent) { - final String uuid = (String) intent.getExtras().get(CONVERSATION); - final String downloadUuid = (String) intent.getExtras().get(MESSAGE); - final String text = intent.getExtras().getString(TEXT, ""); - final String nick = intent.getExtras().getString(NICK, null); - if (selectConversationByUuid(uuid)) { - this.mConversationFragment.reInit(getSelectedConversation()); - if (nick != null) { - this.mConversationFragment.highlightInConference(nick); - } else { - this.mConversationFragment.appendText(text); - } - hideConversationsOverview(); - openConversation(); - if (mContentView instanceof SlidingPaneLayout) { - updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet - } - if (downloadUuid != null) { - final Message message = mSelectedConversation.findMessageWithFileAndUuid(downloadUuid); - if (message != null) { - mConversationFragment.messageListAdapter.startDownloadable(message); - } - } - } - } - - private boolean selectConversationByUuid(String uuid) { - if (uuid == null) { - return false; - } - for (Conversation aConversationList : conversationList) { - if (aConversationList.getUuid().equals(uuid)) { - setSelectedConversation(aConversationList); - return true; - } - } - return false; - } - - @Override - protected void unregisterListeners() { - super.unregisterListeners(); - xmppConnectionService.getNotificationService().setOpenConversation(null); - } - - @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) { - mConversationFragment.hideSnackbar(); - mConversationFragment.updateMessages(); - } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { - mPendingImageUri = data.getData(); - if (xmppConnectionServiceBound) { - attachImageToConversation(getSelectedConversation(),mPendingImageUri); - mPendingImageUri = null; - } - } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) { - mPendingFileUri = data.getData(); - if (xmppConnectionServiceBound) { - attachFileToConversation(getSelectedConversation(),mPendingFileUri); - mPendingFileUri = null; - } - } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO && mPendingImageUri != null) { - if (xmppConnectionServiceBound) { - attachImageToConversation(getSelectedConversation(),mPendingImageUri); - mPendingImageUri = null; - } - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(mPendingImageUri); - sendBroadcast(intent); - } else if (requestCode == ATTACHMENT_CHOICE_LOCATION) { - double latitude = data.getDoubleExtra("latitude",0); - double longitude = data.getDoubleExtra("longitude",0); - this.mPendingGeoUri = Uri.parse("geo:"+String.valueOf(latitude)+","+String.valueOf(longitude)); - if (xmppConnectionServiceBound) { - attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); - this.mPendingGeoUri = null; - } - } - } else { - if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { - mPendingImageUri = null; - } - } - } - - private void attachLocationToConversation(Conversation conversation, Uri uri) { - xmppConnectionService.attachLocationToConversation(conversation,uri, new UiCallback() { - - @Override - public void success(Message message) { - xmppConnectionService.sendMessage(message); - } - - @Override - public void error(int errorCode, Message object) { - - } - - @Override - public void userInputRequried(PendingIntent pi, Message object) { - - } - }); - } - - private void attachFileToConversation(Conversation conversation, Uri uri) { - prepareFileToast = Toast.makeText(getApplicationContext(), - getText(R.string.preparing_file), Toast.LENGTH_LONG); - prepareFileToast.show(); - xmppConnectionService.attachFileToConversation(conversation,uri, new UiCallback() { - @Override - public void success(Message message) { - hidePrepareFileToast(); - xmppConnectionService.sendMessage(message); - } - - @Override - public void error(int errorCode, Message message) { - displayErrorDialog(errorCode); - } - - @Override - public void userInputRequried(PendingIntent pi, Message message) { - - } - }); - } - - private void attachImageToConversation(Conversation conversation, Uri uri) { - prepareFileToast = Toast.makeText(getApplicationContext(), - getText(R.string.preparing_image), Toast.LENGTH_LONG); - prepareFileToast.show(); - xmppConnectionService.attachImageToConversation(conversation, uri, - new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi, - Message object) { - hidePrepareFileToast(); - } - - @Override - public void success(Message message) { - xmppConnectionService.sendMessage(message); - } - - @Override - public void error(int error, Message message) { - hidePrepareFileToast(); - displayErrorDialog(error); - } - }); - } - - private void hidePrepareFileToast() { - if (prepareFileToast != null) { - runOnUiThread(new Runnable() { - - @Override - public void run() { - prepareFileToast.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 (final SendIntentException ignored) { - } - } - - 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 indicateReceived() { - return getPreferences().getBoolean("indicate_received", false); - } - - @Override - protected void refreshUiReal() { - updateConversationList(); - if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 0) { - if (!mRedirected) { - this.mRedirected = true; - startActivity(new Intent(this, EditAccountActivity.class)); - finish(); - } - } else if (conversationList.size() == 0) { - if (!mRedirected) { - this.mRedirected = true; - Intent intent = new Intent(this, StartConversationActivity.class); - intent.putExtra("init",true); - startActivity(intent); - finish(); - } - } else { - ConversationActivity.this.mConversationFragment.updateMessages(); - updateActionBarTitle(); - } - } - - @Override - public void onAccountUpdate() { - this.refreshUi(); - } - - @Override - public void onConversationUpdate() { - this.refreshUi(); - } - - @Override - public void onRosterUpdate() { - this.refreshUi(); - } - - @Override - public void OnUpdateBlocklist(Status status) { - this.refreshUi(); - runOnUiThread(new Runnable() { - @Override - public void run() { - invalidateOptionsMenu(); - } - }); - } - - public void unblockConversation(final Blockable conversation) { - xmppConnectionService.sendUnblockRequest(conversation); - } - - public boolean enterIsSend() { - return getPreferences().getBoolean("enter_is_send",false); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java deleted file mode 100644 index 4f359d9c..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ /dev/null @@ -1,1187 +0,0 @@ -package eu.siacs.conversations.ui; - -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.net.Uri; -import android.os.Bundle; -import android.text.InputType; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -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; -import android.widget.AbsListView.OnScrollListener; -import android.widget.AdapterView; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.PopupWindow; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.TextView.OnEditorActionListener; -import android.widget.Toast; - -import net.java.otr4j.session.SessionStatus; - -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentLinkedQueue; - -import de.tzur.conversations.Settings; -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.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Downloadable; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.DownloadablePlaceholder; -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.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.GeoHelper; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jid.Jid; -import github.ankushsachdeva.emojicon.EmojiconEditText; -import github.ankushsachdeva.emojicon.EmojiconGridView; -import github.ankushsachdeva.emojicon.EmojiconsPopup; -import github.ankushsachdeva.emojicon.emoji.Emojicon; - -public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener { - - protected Conversation conversation; - 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); - } - }); - } - }; - protected ListView messagesView; - final protected List messageList = new ArrayList<>(); - protected MessageAdapter messageListAdapter; - private EditMessage mEditMessage; - private ImageButton mSendButton; - private ImageView mEmojButton; - private View mRootView; - private EmojiconsPopup mEmojPopup; - private RelativeLayout snackbar; - private TextView snackbarMessage; - private TextView snackbarAction; - private boolean messagesLoaded = true; - private Toast messageLoaderToast; - - 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) { - synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { - long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent(); - messagesLoaded = false; - activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { - @Override - public void onMoreMessagesLoaded(final int count, Conversation conversation) { - if (ConversationFragment.this.conversation != conversation) { - return; - } - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - final int oldPosition = messagesView.getFirstVisiblePosition(); - View v = messagesView.getChildAt(0); - final int pxOffset = (v == null) ? 0 : v.getTop(); - ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); - updateStatusMessages(); - messageListAdapter.notifyDataSetChanged(); - if (count != 0) { - final int newPosition = oldPosition + count; - int offset = 0; - try { - Message tmpMessage = messageList.get(newPosition); - - while(tmpMessage.wasMergedIntoPrevious()) { - offset++; - tmpMessage = tmpMessage.prev(); - } - } catch (final IndexOutOfBoundsException ignored) { - - } - messagesView.setSelectionFromTop(newPosition - offset, pxOffset); - messagesLoaded = true; - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - } - } - }); - } - - @Override - public void informUser(final int resId) { - - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - if (ConversationFragment.this.conversation != conversation) { - return; - } - messageLoaderToast = Toast.makeText(activity,resId,Toast.LENGTH_LONG); - messageLoaderToast.show(); - } - }); - - } - }); - - } - } - } - }; - private IntentSender askForPassphraseIntent = null; - 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); - askForPassphraseIntent = null; - } catch (SendIntentException e) { - // - } - } - } - }; - protected OnClickListener clickToVerify = new OnClickListener() { - - @Override - public void onClick(View v) { - activity.verifyOtrSessionDialog(conversation,v); - } - }; - 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(); - } - }; - 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 ConversationActivity activity; - private Message selectedMessage; - - private void sendMessage() { - if (this.conversation == null) { - return; - } - if (mEditMessage.getText().length() < 1) { - if (this.conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextCounterpart(null); - updateChatMsgHint(); - } - return; - } - Message message = new Message(conversation, mEditMessage.getText() - .toString(), conversation.getNextEncryption(activity - .forceEncryption())); - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getNextCounterpart() != null) { - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_PRIVATE); - conversation.setNextCounterpart(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.getNextCounterpart() != null) { - this.mEditMessage.setHint(getString( - R.string.send_private_message_to, - conversation.getNextCounterpart().getResourcepart())); - } 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; - } - getActivity().invalidateOptionsMenu(); - } - } - - private void setupIme() { - if (((ConversationActivity)getActivity()).usingEnterKey()) { - mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE)); - } else { - mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE); - } - } - - @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); - setupIme(); - mEditMessage.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - if (activity != null) { - activity.hideConversationsOverview(); - } - } - }); - mEditMessage.setOnEditorActionListener(mEditorActionListener); - - // Start of emojicon - mEmojButton = (ImageView) view.findViewById(R.id.emoji_btn); - mRootView = view.findViewById(R.id.textsend); - - // Give the topmost view of your activity layout hierarchy. This will be used to measure soft keyboard height - mEmojPopup = new EmojiconsPopup(mRootView, this.getActivity()); - - //Will automatically set size according to the soft keyboard size - mEmojPopup.setSizeForSoftKeyboard(); - - //Set on emojicon click listener - mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() { - - @Override - public void onEmojiconClicked(Emojicon emojicon) { - mEditMessage.append(emojicon.getEmoji()); - } - }); - - //Set on backspace click listener - mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() { - - @Override - public void onEmojiconBackspaceClicked(View v) { - KeyEvent event = new KeyEvent( - 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); - mEditMessage.dispatchKeyEvent(event); - } - }); - - //If the emoji popup is dismissed, change emojiButton to smiley icon - mEmojPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { - - @Override - public void onDismiss() { - changeEmojiKeyboardIcon(mEmojButton, R.drawable.smiley); - } - }); - - //If the text keyboard closes, also dismiss the emoji popup - mEmojPopup.setOnSoftKeyboardOpenCloseListener(new EmojiconsPopup.OnSoftKeyboardOpenCloseListener() { - - @Override - public void onKeyboardOpen(int keyBoardHeight) { - - } - - @Override - public void onKeyboardClose() { - if (mEmojPopup.isShowing()) - mEmojPopup.dismiss(); - } - }); - - //On emoji clicked, add it to edittext - mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() { - - @Override - public void onEmojiconClicked(Emojicon emojicon) { - mEditMessage.append(emojicon.getEmoji()); - } - }); - - //On backspace clicked, emulate the KEYCODE_DEL key event - mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() { - - @Override - public void onEmojiconBackspaceClicked(View v) { - KeyEvent event = new KeyEvent( - 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); - mEditMessage.dispatchKeyEvent(event); - } - }); - - // To toggle between text keyboard and emoji keyboard keyboard(Popup) - mEmojButton.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - - //If popup is not showing => emoji keyboard is not visible, we need to show it - if(!mEmojPopup.isShowing()){ - - //If keyboard is visible, simply show the emoji popup - if(mEmojPopup.isKeyBoardOpen()){ - mEmojPopup.showAtBottom(); - changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard); - } - - //else, open the text keyboard first and immediately after that show the emoji popup - else{ - mEditMessage.setFocusableInTouchMode(true); - mEditMessage.requestFocus(); - mEmojPopup.showAtBottomPending(); - final InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT); - changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard); - } - } - - //If popup is showing, simply dismiss it to show the undelying text keyboard - else{ - mEmojPopup.dismiss(); - } - } - }); - - // End of emojicon - - 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.getCounterpart() != null) { - if (!message.getCounterpart().isBareJid()) { - highlightInConference(message.getCounterpart().getResourcepart()); - } else { - highlightInConference(message.getCounterpart().toString()); - } - } - } else { - activity.switchToContactDetails(message.getContact()); - } - } else { - Account account = message.getConversation().getAccount(); - Intent intent = new Intent(activity, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().toBareJid().toString()); - startActivity(intent); - } - } - }); - 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.getCounterpart() != null) { - privateMessageWith(message.getCounterpart()); - } - } - } else { - activity.showQrCode(); - } - } - }); - messagesView.setAdapter(messageListAdapter); - - registerForContextMenu(messagesView); - - return view; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - synchronized (this.messageList) { - super.onCreateContextMenu(menu, v, menuInfo); - AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; - this.selectedMessage = this.messageList.get(acmi.position); - populateContextMenu(menu); - } - } - - private void populateContextMenu(ContextMenu menu) { - final Message m = this.selectedMessage; - if (m.getType() != Message.TYPE_STATUS) { - activity.getMenuInflater().inflate(R.menu.message_context, menu); - menu.setHeaderTitle(R.string.message_options); - MenuItem copyText = menu.findItem(R.id.copy_text); - MenuItem shareWith = menu.findItem(R.id.share_with); - MenuItem sendAgain = menu.findItem(R.id.send_again); - MenuItem copyUrl = menu.findItem(R.id.copy_url); - MenuItem downloadImage = menu.findItem(R.id.download_image); - MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); - if ((m.getType() != Message.TYPE_TEXT && m.getType() != Message.TYPE_PRIVATE) - || m.getDownloadable() != null || GeoHelper.isGeoUri(m.getBody())) { - copyText.setVisible(false); - } - if ((m.getType() == Message.TYPE_TEXT - || m.getType() == Message.TYPE_PRIVATE - || m.getDownloadable() != null) - && (!GeoHelper.isGeoUri(m.getBody()))) { - shareWith.setVisible(false); - } - if (m.getStatus() != Message.STATUS_SEND_FAILED) { - sendAgain.setVisible(false); - } - if (((m.getType() != Message.TYPE_IMAGE && m.getDownloadable() == null) - || m.getImageParams().url == null) && !GeoHelper.isGeoUri(m.getBody())) { - copyUrl.setVisible(false); - } - if (m.getType() != Message.TYPE_TEXT - || m.getDownloadable() != null - || !m.bodyContainsDownloadable()) { - downloadImage.setVisible(false); - } - if (!((m.getDownloadable() != null && !(m.getDownloadable() instanceof DownloadablePlaceholder)) - || (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING - || m.getStatus() == Message.STATUS_OFFERED)))) { - cancelTransmission.setVisible(false); - } - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.share_with: - shareWith(selectedMessage); - return true; - case R.id.copy_text: - copyText(selectedMessage); - return true; - case R.id.send_again: - resendMessage(selectedMessage); - return true; - case R.id.copy_url: - copyUrl(selectedMessage); - return true; - case R.id.download_image: - downloadImage(selectedMessage); - return true; - case R.id.cancel_transmission: - cancelTransmission(selectedMessage); - return true; - default: - return super.onContextItemSelected(item); - } - } - - private void shareWith(Message message) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - if (GeoHelper.isGeoUri(message.getBody())) { - shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); - shareIntent.setType("text/plain"); - } else { - shareIntent.putExtra(Intent.EXTRA_STREAM, - activity.xmppConnectionService.getFileBackend() - .getJingleFileUri(message)); - shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - String path = message.getRelativeFilePath(); - String mime = path == null ? null : URLConnection.guessContentTypeFromName(path); - if (mime == null) { - mime = "image/webp"; - } - shareIntent.setType(mime); - } - activity.startActivity(Intent.createChooser(shareIntent,getText(R.string.share_with))); - } - - private void copyText(Message message) { - if (activity.copyTextToClipboard(message.getMergedBody(), - R.string.message_text)) { - Toast.makeText(activity, R.string.message_copied_to_clipboard, - Toast.LENGTH_SHORT).show(); - } - } - - private void resendMessage(Message message) { - if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - if (!file.exists()) { - Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); - message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); - return; - } - } - activity.xmppConnectionService.resendFailedMessages(message); - } - - private void copyUrl(Message message) { - final String url; - final int resId; - if (GeoHelper.isGeoUri(message.getBody())) { - resId = R.string.location; - url = message.getBody(); - } else { - resId = R.string.image_url; - url = message.getImageParams().url.toString(); - } - if (activity.copyTextToClipboard(url, resId)) { - Toast.makeText(activity, R.string.url_copied_to_clipboard, - Toast.LENGTH_SHORT).show(); - } - } - - private void downloadImage(Message message) { - activity.xmppConnectionService.getHttpConnectionManager() - .createNewConnection(message); - } - - private void cancelTransmission(Message message) { - Downloadable downloadable = message.getDownloadable(); - if (downloadable!=null) { - downloadable.cancel(); - } else { - activity.xmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED); - } - } - - protected void privateMessageWith(final Jid counterpart) { - this.mEditMessage.setText(""); - this.conversation.setNextCounterpart(counterpart); - updateChatMsgHint(); - } - - protected void highlightInConference(String nick) { - String oldString = mEditMessage.getText().toString(); - 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 onStop() { - mDecryptJobRunning = false; - super.onStop(); - if (this.conversation != null) { - final String msg = mEditMessage.getText().toString(); - this.conversation.setNextMessage(msg); - updateChatState(this.conversation,msg); - } - } - - private void updateChatState(final Conversation conversation, final String msg) { - ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED; - Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) { - activity.xmppConnectionService.sendChatState(conversation); - } - } - - public void reInit(Conversation conversation) { - if (conversation == null) { - return; - } - - this.activity = (ConversationActivity) getActivity(); - - if (this.conversation != null) { - final String msg = mEditMessage.getText().toString(); - this.conversation.setNextMessage(msg); - if (this.conversation != conversation) { - updateChatState(this.conversation,msg); - } - this.conversation.trim(); - } - - this.askForPassphraseIntent = null; - this.conversation = conversation; - this.mDecryptJobRunning = false; - this.mEncryptedMessages.clear(); - if (this.conversation.getMode() == Conversation.MODE_MULTI) { - this.conversation.setNextCounterpart(null); - } - this.mEditMessage.setKeyboardListener(null); - this.mEditMessage.setText(""); - this.mEditMessage.append(this.conversation.getNextMessage()); - this.mEditMessage.setKeyboardListener(this); - this.messagesView.setAdapter(messageListAdapter); - updateMessages(); - this.messagesLoaded = true; - int size = this.messageList.size(); - if (size > 0) { - messagesView.setSelection(size - 1); - } - } - - private OnClickListener mUnblockClickListener = new OnClickListener() { - @Override - public void onClick(final View v) { - v.post(new Runnable() { - @Override - public void run() { - v.setVisibility(View.INVISIBLE); - } - }); - if (conversation.isDomainBlocked()) { - BlockContactDialog.show(activity, activity.xmppConnectionService, conversation); - } else { - activity.unblockConversation(conversation); - } - } - }; - - private OnClickListener mAddBackClickListener = new OnClickListener() { - - @Override - public void onClick(View v) { - final Contact contact = conversation == null ? null :conversation.getContact(); - if (contact != null) { - activity.xmppConnectionService.createContact(contact); - activity.switchToContactDetails(contact); - } - } - }; - - private OnClickListener mUnmuteClickListener = new OnClickListener() { - - @Override - public void onClick(final View v) { - activity.unmuteConversation(conversation); - } - }; - - private OnClickListener mAnswerSmpClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = new Intent(activity, VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); - intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); - intent.putExtra("mode",VerifyOTRActivity.MODE_ANSWER_QUESTION); - startActivity(intent); - } - }; - - private void updateSnackBar(final Conversation conversation) { - final Account account = conversation.getAccount(); - final Contact contact = conversation.getContact(); - final int mode = conversation.getMode(); - if (conversation.isBlocked()) { - showSnackbar(R.string.contact_blocked, R.string.unblock,this.mUnblockClickListener); - } else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_added_you, R.string.add_back,this.mAddBackClickListener); - } else if (mode == Conversation.MODE_MULTI - &&!conversation.getMucOptions().online() - && account.getStatus() == Account.State.ONLINE) { - switch (conversation.getMucOptions().getError()) { - case MucOptions.ERROR_NICK_IN_USE: - showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc); - break; - case MucOptions.ERROR_UNKNOWN: - 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; - } - } else if (askForPassphraseIntent != null ) { - showSnackbar(R.string.openpgp_messages_found,R.string.decrypt, clickToDecryptListener); - } else if (mode == Conversation.MODE_SINGLE - && conversation.smpRequested()) { - showSnackbar(R.string.smp_requested, R.string.verify,this.mAnswerSmpClickListener); - } else if (mode == Conversation.MODE_SINGLE - &&conversation.hasValidOtrSession() - && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) - && (!conversation.isOtrFingerprintVerified())) { - showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); - } else if (conversation.isMuted()) { - showSnackbar(R.string.notifications_disabled, R.string.enable,this.mUnmuteClickListener); - } else { - hideSnackbar(); - } - } - - public void updateMessages() { - synchronized (this.messageList) { - if (getView() == null) { - return; - } - final ConversationActivity activity = (ConversationActivity) getActivity(); - if (this.conversation != null) { - updateSnackBar(this.conversation); - final Contact contact = this.conversation.getContact(); - if (this.conversation.isBlocked()) { - - } else if (!contact.showInRoster() - && contact - .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - - } else if (conversation.getMode() == Conversation.MODE_SINGLE) { - makeFingerprintWarning(); - } else if (!conversation.getMucOptions().online() - && conversation.getAccount().getStatus() == Account.State.ONLINE) { - - } else if (this.conversation.isMuted()) { - - } - conversation.populateWithMessages(ConversationFragment.this.messageList); - for (final Message message : this.messageList) { - if (message.getEncryption() == Message.ENCRYPTION_PGP - && (message.getStatus() == Message.STATUS_RECEIVED || message - .getStatus() >= Message.STATUS_SEND) - && message.getDownloadable() == null) { - if (!mEncryptedMessages.contains(message)) { - mEncryptedMessages.add(message); - } - } - } - decryptNext(); - updateStatusMessages(); - this.messageListAdapter.notifyDataSetChanged(); - updateChatMsgHint(); - if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) { - activity.sendReadMarkerIfNecessary(conversation); - } - 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(); - updateSnackBar(conversation); - } - - @Override - public void success(Message message) { - mDecryptJobRunning = false; - try { - mEncryptedMessages.remove(); - } catch (final NoSuchElementException ignored) { - - } - activity.xmppConnectionService.updateMessage(message); - } - - @Override - public void error(int error, Message message) { - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - mDecryptJobRunning = false; - try { - mEncryptedMessages.remove(); - } catch (final NoSuchElementException ignored) { - - } - 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 (Settings.SHOW_ONLINE_STATUS && c != null - && c.getAccount().getStatus() == Account.State.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() { - synchronized (this.messageList) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - ChatState state = conversation.getIncomingChatState(); - if (state == ChatState.COMPOSING) { - this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName()))); - } else if (state == ChatState.PAUSED) { - this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName()))); - } else { - 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, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); - return; - } - } - } - } - } - } - } - - protected void makeFingerprintWarning() { - - } - - protected void showSnackbar(final int message, final int action, - final OnClickListener clickListener) { - snackbar.setVisibility(View.VISIBLE); - snackbar.setOnClickListener(null); - snackbarMessage.setText(message); - snackbarMessage.setOnClickListener(null); - snackbarAction.setVisibility(View.VISIBLE); - 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; - activity.selectPresence(message.getConversation(), - new OnPresenceSelected() { - - @Override - public void onPresenceSelected() { - message.setCounterpart(conversation.getNextCounterpart()); - xmppService.sendMessage(message); - messageSent(); - } - }); - } - - public void appendText(String text) { - String previous = this.mEditMessage.getText().toString(); - if (previous.length() != 0 && !previous.endsWith(" ")) { - text = " " + text; - } - this.mEditMessage.append(text); - } - - @Override - public boolean onEnterPressed() { - if (activity.enterIsSend()) { - sendMessage(); - return true; - } else { - return false; - } - } - - @Override - public void onTypingStarted() { - Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) { - activity.xmppConnectionService.sendChatState(conversation); - } - } - - @Override - public void onTypingStopped() { - Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) { - activity.xmppConnectionService.sendChatState(conversation); - } - } - - @Override - public void onTextDeleted() { - Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { - activity.xmppConnectionService.sendChatState(conversation); - } - } - - private void changeEmojiKeyboardIcon(ImageView iconToBeChanged, int drawableResourceId){ - iconToBeChanged.setImageResource(drawableResourceId); - } - -} diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java deleted file mode 100644 index 27dfc492..00000000 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ /dev/null @@ -1,505 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.PendingIntent; -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.View.OnClickListener; -import android.widget.AutoCompleteTextView; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TableLayout; -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.CryptoHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.XmppConnection.Features; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; -import eu.siacs.conversations.xmpp.pep.Avatar; - -public class EditAccountActivity extends XmppActivity implements OnAccountUpdate{ - - private AutoCompleteTextView mAccountJid; - private EditText mPassword; - private EditText mPasswordConfirm; - private CheckBox mRegisterNew; - private Button mCancelButton; - private Button mSaveButton; - private TableLayout mMoreTable; - - private LinearLayout mStats; - private TextView mServerInfoSm; - private TextView mServerInfoRosterVersion; - private TextView mServerInfoCarbons; - private TextView mServerInfoMam; - private TextView mServerInfoCSI; - private TextView mServerInfoBlocking; - private TextView mServerInfoPep; - private TextView mSessionEst; - private TextView mOtrFingerprint; - private ImageView mAvatar; - private RelativeLayout mOtrFingerprintBox; - private ImageButton mOtrFingerprintToClipboardButton; - - private Jid jidToEdit; - private Account mAccount; - - private boolean mFetchingAvatar = false; - - private final OnClickListener mSaveButtonClickListener = new OnClickListener() { - - @Override - public void onClick(final View v) { - if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED) { - mAccount.setOption(Account.OPTION_DISABLED, false); - xmppConnectionService.updateAccount(mAccount); - return; - } - final boolean registerNewAccount = mRegisterNew.isChecked(); - final Jid jid; - try { - jid = Jid.fromString(mAccountJid.getText().toString()); - } catch (final InvalidJidException e) { - mAccountJid.setError(getString(R.string.invalid_jid)); - mAccountJid.requestFocus(); - return; - } - if (jid.isDomainJid()) { - mAccountJid.setError(getString(R.string.invalid_jid)); - mAccountJid.requestFocus(); - return; - } - final String password = mPassword.getText().toString(); - final 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) { - try { - mAccount.setUsername(jid.hasLocalpart() ? jid.getLocalpart() : ""); - mAccount.setServer(jid.getDomainpart()); - } catch (final InvalidJidException ignored) { - return; - } - mAccountJid.setError(null); - mPasswordConfirm.setError(null); - mAccount.setPassword(password); - mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); - xmppConnectionService.updateAccount(mAccount); - } else { - try { - if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) { - mAccountJid.setError(getString(R.string.account_already_exists)); - mAccountJid.requestFocus(); - return; - } - } catch (final InvalidJidException e) { - return; - } - mAccount = new Account(jid.toBareJid(), 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 final OnClickListener mCancelButtonClickListener = new OnClickListener() { - - @Override - public void onClick(final View v) { - finish(); - } - }; - @Override - public void onAccountUpdate() { - runOnUiThread(new Runnable() { - - @Override - public void run() { - invalidateOptionsMenu(); - if (mAccount != null - && mAccount.getStatus() != Account.State.ONLINE - && mFetchingAvatar) { - startActivity(new Intent(getApplicationContext(), - ManageAccountActivity.class)); - finish(); - } else if (jidToEdit == null && mAccount != null - && mAccount.getStatus() == Account.State.ONLINE) { - if (!mFetchingAvatar) { - mFetchingAvatar = true; - xmppConnectionService.checkForAvatar(mAccount, - mAvatarFetchCallback); - } - } else { - updateSaveButton(); - } - if (mAccount != null) { - updateAccountInformation(); - } - } - }); - } - private final UiCallback mAvatarFetchCallback = new UiCallback() { - - @Override - public void userInputRequried(final PendingIntent pi, final Avatar avatar) { - finishInitialSetup(avatar); - } - - @Override - public void success(final Avatar avatar) { - finishInitialSetup(avatar); - } - - @Override - public void error(final int errorCode, final Avatar avatar) { - finishInitialSetup(avatar); - } - }; - private final TextWatcher mTextWatcher = new TextWatcher() { - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - updateSaveButton(); - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { - } - - @Override - public void afterTextChanged(final Editable s) { - - } - }; - - private final OnClickListener mAvatarClickListener = new OnClickListener() { - @Override - public void onClick(final View view) { - if (mAccount != null) { - final Intent intent = new Intent(getApplicationContext(), - PublishProfilePictureActivity.class); - intent.putExtra("account", mAccount.getJid().toBareJid().toString()); - startActivity(intent); - } - } - }; - - protected void finishInitialSetup(final Avatar avatar) { - runOnUiThread(new Runnable() { - - @Override - public void run() { - final Intent intent; - if (avatar != null) { - intent = new Intent(getApplicationContext(), - StartConversationActivity.class); - intent.putExtra("init",true); - } else { - intent = new Intent(getApplicationContext(), - PublishProfilePictureActivity.class); - intent.putExtra("account", mAccount.getJid().toBareJid().toString()); - intent.putExtra("setup", true); - } - startActivity(intent); - finish(); - } - }); - } - - protected void updateSaveButton() { - if (mAccount != null && (mAccount.getStatus() == Account.State.CONNECTING || mFetchingAvatar)) { - this.mSaveButton.setEnabled(false); - this.mSaveButton.setTextColor(getSecondaryTextColor()); - this.mSaveButton.setText(R.string.account_status_connecting); - } else if (mAccount != null && mAccount.getStatus() == Account.State.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.isOnlineAndConnected()) { - 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().toBareJid().toString().equals( - this.mAccountJid.getText().toString())) - || (!this.mAccount.getPassword().equals( - this.mPassword.getText().toString())); - } - - @Override - protected String getShareableUri() { - if (mAccount!=null) { - return mAccount.getShareableUri(); - } else { - return ""; - } - } - - @Override - protected void onCreate(final 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.mAvatar = (ImageView) findViewById(R.id.avater); - this.mAvatar.setOnClickListener(this.mAvatarClickListener); - 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.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version); - this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); - this.mServerInfoMam = (TextView) findViewById(R.id.server_info_mam); - this.mServerInfoCSI = (TextView) findViewById(R.id.server_info_csi); - this.mServerInfoBlocking = (TextView) findViewById(R.id.server_info_blocking); - 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.mMoreTable = (TableLayout) findViewById(R.id.server_info_more); - final OnCheckedChangeListener OnCheckedShowConfirmPassword = new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(final CompoundButton buttonView, - final boolean isChecked) { - if (isChecked) { - mPasswordConfirm.setVisibility(View.VISIBLE); - } else { - mPasswordConfirm.setVisibility(View.GONE); - } - updateSaveButton(); - } - }; - this.mRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.editaccount, menu); - final MenuItem showQrCode = menu.findItem(R.id.action_show_qr_code); - final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list); - final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more); - final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server); - if (mAccount != null && mAccount.isOnlineAndConnected()) { - if (!mAccount.getXmppConnection().getFeatures().blocking()) { - showBlocklist.setVisible(false); - } - if (!mAccount.getXmppConnection().getFeatures().register()) { - changePassword.setVisible(false); - } - } else { - showQrCode.setVisible(false); - showBlocklist.setVisible(false); - showMoreInfo.setVisible(false); - changePassword.setVisible(false); - } - return true; - } - - @Override - protected void onStart() { - super.onStart(); - if (getIntent() != null) { - try { - this.jidToEdit = Jid.fromString(getIntent().getStringExtra("jid")); - } catch (final InvalidJidException | NullPointerException ignored) { - this.jidToEdit = null; - } - if (this.jidToEdit != null) { - this.mRegisterNew.setVisibility(View.GONE); - if (getActionBar() != null) { - getActionBar().setTitle(getString(R.string.account_details)); - } - } else { - this.mAvatar.setVisibility(View.GONE); - if (getActionBar() != null) { - getActionBar().setTitle(R.string.action_add_account); - } - } - } - } - - @Override - protected void onBackendConnected() { - final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, - android.R.layout.simple_list_item_1, - xmppConnectionService.getKnownHosts()); - if (this.jidToEdit != null) { - this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); - updateAccountInformation(); - } else if (this.xmppConnectionService.getAccounts().size() == 0) { - if (getActionBar() != null) { - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setDisplayShowHomeEnabled(false); - getActionBar().setHomeButtonEnabled(false); - } - this.mCancelButton.setEnabled(false); - this.mCancelButton.setTextColor(getSecondaryTextColor()); - } - this.mAccountJid.setAdapter(mKnownHostsAdapter); - updateSaveButton(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_show_block_list: - final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class); - showBlocklistIntent.putExtra("account", mAccount.getJid().toString()); - startActivity(showBlocklistIntent); - break; - case R.id.action_server_info_show_more: - mMoreTable.setVisibility(item.isChecked() ? View.GONE : View.VISIBLE); - item.setChecked(!item.isChecked()); - break; - case R.id.action_change_password_on_server: - final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class); - changePasswordIntent.putExtra("account", mAccount.getJid().toString()); - startActivity(changePasswordIntent); - break; - } - return super.onOptionsItemSelected(item); - } - - private void updateAccountInformation() { - this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString()); - this.mPassword.setText(this.mAccount.getPassword()); - if (this.jidToEdit != null) { - this.mAvatar.setVisibility(View.VISIBLE); - this.mAvatar.setImageBitmap(avatarService().get(this.mAccount, getPixel(72))); - } - 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.isOnlineAndConnected() && !this.mFetchingAvatar) { - this.mStats.setVisibility(View.VISIBLE); - this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() - .getLastSessionEstablished())); - Features features = this.mAccount.getXmppConnection().getFeatures(); - if (features.rosterVersioning()) { - this.mServerInfoRosterVersion.setText(R.string.server_info_available); - } else { - this.mServerInfoRosterVersion.setText(R.string.server_info_unavailable); - } - if (features.carbons()) { - this.mServerInfoCarbons.setText(R.string.server_info_available); - } else { - this.mServerInfoCarbons - .setText(R.string.server_info_unavailable); - } - if (features.mam()) { - this.mServerInfoMam.setText(R.string.server_info_available); - } else { - this.mServerInfoMam.setText(R.string.server_info_unavailable); - } - if (features.csi()) { - this.mServerInfoCSI.setText(R.string.server_info_available); - } else { - this.mServerInfoCSI.setText(R.string.server_info_unavailable); - } - if (features.blocking()) { - this.mServerInfoBlocking.setText(R.string.server_info_available); - } else { - this.mServerInfoBlocking.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(); - if (fingerprint != null) { - this.mOtrFingerprintBox.setVisibility(View.VISIBLE); - this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint)); - this.mOtrFingerprintToClipboardButton - .setVisibility(View.VISIBLE); - this.mOtrFingerprintToClipboardButton - .setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(final View v) { - - if (copyTextToClipboard(fingerprint, R.string.otr_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.getStatus().getReadableId())); - this.mAccountJid.requestFocus(); - } else { - this.mAccountJid.setError(null); - } - this.mStats.setVisibility(View.GONE); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java deleted file mode 100644 index a7aa2024..00000000 --- a/src/main/java/eu/siacs/conversations/ui/EditMessage.java +++ /dev/null @@ -1,79 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.os.Handler; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.widget.EditText; - -import eu.siacs.conversations.Config; -import github.ankushsachdeva.emojicon.EmojiconEditText; - -public class EditMessage extends EmojiconEditText { - - public EditMessage(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public EditMessage(Context context) { - super(context); - } - - protected Handler mTypingHandler = new Handler(); - - protected Runnable mTypingTimeout = new Runnable() { - @Override - public void run() { - if (isUserTyping && keyboardListener != null) { - keyboardListener.onTypingStopped(); - isUserTyping = false; - } - } - }; - - private boolean isUserTyping = false; - - protected KeyboardListener keyboardListener; - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_ENTER) { - if (keyboardListener != null && keyboardListener.onEnterPressed()) { - return true; - } - } - return super.onKeyDown(keyCode, event); - } - - @Override - public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { - super.onTextChanged(text,start,lengthBefore,lengthAfter); - if (this.mTypingHandler != null && this.keyboardListener != null) { - this.mTypingHandler.removeCallbacks(mTypingTimeout); - this.mTypingHandler.postDelayed(mTypingTimeout, Config.TYPING_TIMEOUT * 1000); - final int length = text.length(); - if (!isUserTyping && length > 0) { - this.isUserTyping = true; - this.keyboardListener.onTypingStarted(); - } else if (length == 0) { - this.isUserTyping = false; - this.keyboardListener.onTextDeleted(); - } - } - } - - public void setKeyboardListener(KeyboardListener listener) { - this.keyboardListener = listener; - if (listener != null) { - this.isUserTyping = false; - } - } - - public interface KeyboardListener { - public boolean onEnterPressed(); - public void onTypingStarted(); - public void onTypingStopped(); - public void onTextDeleted(); - } - -} diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java deleted file mode 100644 index b2d5ddfd..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ /dev/null @@ -1,273 +0,0 @@ -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 implements OnAccountUpdate { - - protected Account selectedAccount = null; - - protected final List accountList = new ArrayList<>(); - protected ListView accountListView; - protected AccountAdapter mAccountAdapter; - - @Override - public void onAccountUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - synchronized (this.accountList) { - accountList.clear(); - accountList.addAll(xmppConnectionService.getAccounts()); - } - invalidateOptionsMenu(); - 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().toBareJid().toString()); - } - - @Override - void onBackendConnected() { - this.accountList.clear(); - this.accountList.addAll(xmppConnectionService.getAccounts()); - mAccountAdapter.notifyDataSetChanged(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.manageaccounts, menu); - MenuItem enableAll = menu.findItem(R.id.action_enable_all); - if (!accountsLeftToEnable()) { - enableAll.setVisible(false); - } - MenuItem disableAll = menu.findItem(R.id.action_disable_all); - if (!accountsLeftToDisable()) { - disableAll.setVisible(false); - } - 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); - return true; - 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; - case R.id.action_disable_all: - disableAllAccounts(); - break; - case R.id.action_enable_all: - enableAllAccounts(); - 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().toString()); - startActivity(intent); - } - - private void disableAllAccounts() { - List list = new ArrayList<>(); - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - list.add(account); - } - } - } - for(Account account : list) { - disableAccount(account); - } - } - - private boolean accountsLeftToDisable() { - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - return true; - } - } - return false; - } - } - - private boolean accountsLeftToEnable() { - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (account.isOptionSet(Account.OPTION_DISABLED)) { - return true; - } - } - return false; - } - } - - private void enableAllAccounts() { - List list = new ArrayList<>(); - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (account.isOptionSet(Account.OPTION_DISABLED)) { - list.add(account); - } - } - } - for(Account account : list) { - enableAccount(account); - } - } - - 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/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java deleted file mode 100644 index 3f72b723..00000000 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ /dev/null @@ -1,253 +0,0 @@ -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 android.widget.Toast; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; -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 OnLongClickListener backToDefaultListener = new OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - avatarUri = defaultUri; - loadImageIntoPreview(defaultUri); - return true; - } - }; - 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) { - Intent intent = new Intent(getApplicationContext(), - StartConversationActivity.class); - intent.putExtra("init",true); - startActivity(intent); - } - Toast.makeText(PublishProfilePictureActivity.this, - R.string.avatar_has_been_published, - Toast.LENGTH_SHORT).show(); - 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) { - } - }; - - @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) { - Intent intent = new Intent(getApplicationContext(), - StartConversationActivity.class); - intent.putExtra("init",true); - startActivity(intent); - } - 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) { - Jid jid; - try { - jid = Jid.fromString(getIntent().getStringExtra("account")); - } catch (InvalidJidException e) { - jid = null; - } - 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().toBareJid().toString()); - } - } - - } - - @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/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java deleted file mode 100644 index 2115b23b..00000000 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ /dev/null @@ -1,92 +0,0 @@ -package eu.siacs.conversations.ui; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - -import de.tzur.conversations.Settings; -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) { - // need to synchronize the settings class first - Settings.synchronizeSettingsClassWithPreferences(getPreferences(), name); - switch (name) { - case "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.reconnectAccountInBackground(account); - } - } - } - break; - case "keep_foreground_service": - xmppConnectionService.toggleForegroundService(); - break; - case "confirm_messages_list": - if (xmppConnectionServiceBound) { - for (Account account : xmppConnectionService.getAccounts()) { - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - xmppConnectionService.sendPresence(account); - } - } - } - break; - } - } - -} diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java deleted file mode 100644 index e4185abc..00000000 --- a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java +++ /dev/null @@ -1,65 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Dialog; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import eu.siacs.conversations.R; - -public class SettingsFragment extends PreferenceFragment { - - //http://stackoverflow.com/questions/16374820/action-bar-home-button-not-functional-with-nested-preferencescreen/16800527#16800527 - private void initializeActionBar(PreferenceScreen preferenceScreen) { - final Dialog dialog = preferenceScreen.getDialog(); - - if (dialog != null) { - View homeBtn = dialog.findViewById(android.R.id.home); - - if (homeBtn != null) { - View.OnClickListener dismissDialogClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - dialog.dismiss(); - } - }; - - ViewParent homeBtnContainer = homeBtn.getParent(); - - if (homeBtnContainer instanceof FrameLayout) { - ViewGroup containerParent = (ViewGroup) homeBtnContainer.getParent(); - if (containerParent instanceof LinearLayout) { - ((LinearLayout) containerParent).setOnClickListener(dismissDialogClickListener); - } else { - ((FrameLayout) homeBtnContainer).setOnClickListener(dismissDialogClickListener); - } - } else { - homeBtn.setOnClickListener(dismissDialogClickListener); - } - } - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Load the preferences from an XML resource - addPreferencesFromResource(R.xml.preferences); - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - super.onPreferenceTreeClick(preferenceScreen, preference); - if (preference instanceof PreferenceScreen) { - initializeActionBar((PreferenceScreen) preference); - } - return false; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java deleted file mode 100644 index 6be238dc..00000000 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ /dev/null @@ -1,222 +0,0 @@ -package eu.siacs.conversations.ui; - -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; - -import java.io.UnsupportedEncodingException; -import java.net.URLConnection; -import java.net.URLDecoder; -import java.nio.charset.UnsupportedCharsetException; -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 eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class ShareWithActivity extends XmppActivity { - - private class Share { - public Uri uri; - public boolean image; - 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 attachFileCallback = 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); - - if (getActionBar() != null) { - 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(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_add: - final Intent intent = new Intent(getApplicationContext(), - ChooseContactActivity.class); - startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onStart() { - final String type = getIntent().getType(); - final Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); - if (type != null && uri != null && !type.equalsIgnoreCase("text/plain")) { - this.share.uri = uri; - this.share.image = type.startsWith("image/") || isImage(uri); - } else { - this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT); - } - if (xmppConnectionServiceBound) { - xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uri == null); - } - super.onStart(); - } - - protected boolean isImage(Uri uri) { - try { - String guess = URLConnection.guessContentTypeFromName(uri.toString()); - return (guess != null && guess.startsWith("image/")); - } catch (final StringIndexOutOfBoundsException ignored) { - return false; - } - } - - @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; - try { - account = xmppConnectionService.findAccountByJid(Jid.fromString(share.account)); - } catch (final InvalidJidException e) { - account = null; - } - if (account == null) { - return; - } - final Conversation conversation; - try { - conversation = xmppConnectionService - .findOrCreateConversation(account, Jid.fromString(share.contact), false); - } catch (final InvalidJidException e) { - return; - } - share(conversation); - } - - private void share(final Conversation conversation) { - if (share.uri != null) { - selectPresence(conversation, new OnPresenceSelected() { - @Override - public void onPresenceSelected() { - if (share.image) { - Toast.makeText(getApplicationContext(), - getText(R.string.preparing_image), - Toast.LENGTH_LONG).show(); - ShareWithActivity.this.xmppConnectionService - .attachImageToConversation(conversation, share.uri, - attachFileCallback); - } else { - Toast.makeText(getApplicationContext(), - getText(R.string.preparing_file), - Toast.LENGTH_LONG).show(); - ShareWithActivity.this.xmppConnectionService - .attachFileToConversation(conversation, share.uri, - attachFileCallback); - } - switchToConversation(conversation, null, true); - finish(); - } - }); - - } else { - switchToConversation(conversation, this.share.text, true); - finish(); - } - - } - -} diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java deleted file mode 100644 index a556b8b7..00000000 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ /dev/null @@ -1,819 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -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.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.os.Build; -import android.os.Bundle; -import android.os.Parcelable; -import android.support.v13.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -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.Checkable; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.Spinner; - -import com.google.zxing.integration.android.IntentIntegrator; -import com.google.zxing.integration.android.IntentResult; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Blockable; -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.Presences; -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.XmppUri; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class StartConversationActivity extends XmppActivity implements OnRosterUpdate, OnUpdateBlocklist { - - public int conference_context_id; - public int contact_context_id; - 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 Invite mPendingInvite = null; - private Menu mOptionsMenu; - private EditText mSearchEditText; - 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 boolean mHideOfflineContacts = false; - 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) { - if (getActionBar() != null) { - getActionBar().setSelectedNavigationItem(position); - } - onTabChanged(); - } - }; - 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 MenuItem mMenuSearchView; - private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { - @Override - public void onTagClicked(String tag) { - if (mMenuSearchView != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.setText(""); - mSearchEditText.append(tag); - filter(tag); - } - } - }; - private String mInitialJid; - - @Override - public void onRosterUpdate() { - this.refreshUi(); - } - - @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); - ((ListItemAdapter) mContactsAdapter).setOnTagClickedListener(this.mOnTagClickedListener); - 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); - } - }); - - this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false); - - } - - 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 toggleContactBlock() { - final int position = contact_context_id; - BlockContactDialog.show(this, xmppConnectionService, (Contact)contacts.get(position)); - } - - protected void deleteContact() { - final int position = contact_context_id; - final Contact contact = (Contact) contacts.get(position); - final 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(final String prefilledJid, final String fingerprint) { - 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); - if (fingerprint!=null) { - jid.setFocusable(false); - jid.setFocusableInTouchMode(false); - jid.setClickable(false); - jid.setCursorVisible(false); - } - } - 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(final View v) { - if (!xmppConnectionServiceBound) { - return; - } - final Jid accountJid; - try { - accountJid = Jid.fromString((String) spinner.getSelectedItem()); - } catch (final InvalidJidException e) { - return; - } - final Jid contactJid; - try { - contactJid = Jid.fromString(jid.getText().toString()); - } catch (final InvalidJidException e) { - jid.setError(getString(R.string.invalid_jid)); - return; - } - final Account account = xmppConnectionService - .findAccountByJid(accountJid); - if (account == null) { - dialog.dismiss(); - return; - } - final Contact contact = account.getRoster().getContact(contactJid); - if (contact.showInRoster()) { - jid.setError(getString(R.string.contact_already_exists)); - } else { - contact.addOtrFingerprint(fingerprint); - xmppConnectionService.createContact(contact); - dialog.dismiss(); - switchToConversation(contact); - } - } - }); - - } - - @SuppressLint("InflateParams") - protected void showJoinConferenceDialog(final String prefilledJid) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.join_conference); - final 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)); - if (prefilledJid != null) { - jid.append(prefilledJid); - } - populateAccountSpinner(spinner); - final Checkable 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(final View v) { - if (!xmppConnectionServiceBound) { - return; - } - final Jid accountJid; - try { - accountJid = Jid.fromString((String) spinner.getSelectedItem()); - } catch (final InvalidJidException e) { - return; - } - final Jid conferenceJid; - try { - conferenceJid = Jid.fromString(jid.getText().toString()); - } catch (final InvalidJidException e) { - jid.setError(getString(R.string.invalid_jid)); - return; - } - final 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 { - final Bookmark bookmark = new Bookmark(account,conferenceJid.toBareJid()); - bookmark.setAutojoin(true); - account.getBookmarks().add(bookmark); - xmppConnectionService - .pushBookmarks(account); - final Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, - conferenceJid, true); - conversation.setBookmark(bookmark); - if (!conversation.getMucOptions().online()) { - xmppConnectionService - .joinMuc(conversation); - } - dialog.dismiss(); - switchToConversation(conversation); - } - } else { - final Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, - conferenceJid, true); - if (!conversation.getMucOptions().online()) { - xmppConnectionService.joinMuc(conversation); - } - dialog.dismiss(); - switchToConversation(conversation); - } - } - }); - } - - 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 = menu.findItem(R.id.action_create_contact); - MenuItem menuCreateConference = menu.findItem(R.id.action_join_conference); - MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline); - menuHideOffline.setChecked(this.mHideOfflineContacts); - mMenuSearchView = 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,null); - return true; - case R.id.action_join_conference: - showJoinConferenceDialog(null); - return true; - case R.id.action_scan_qr_code: - new IntentIntegrator(this).initiateScan(); - return true; - case R.id.action_hide_offline: - mHideOfflineContacts = !item.isChecked(); - getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit(); - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } - invalidateOptionsMenu(); - } - 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 - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); - if (scanResult != null && scanResult.getFormatName() != null) { - String data = scanResult.getContents(); - Invite invite = new Invite(data); - if (xmppConnectionServiceBound) { - invite.invite(); - } else if (invite.getJid() != null) { - this.mPendingInvite = invite; - } else { - this.mPendingInvite = null; - } - } - } - super.onActivityResult(requestCode, requestCode, intent); - } - - @Override - protected void onBackendConnected() { - this.mActivatedAccounts.clear(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - this.mActivatedAccounts.add(account.getJid().toBareJid().toString()); - } - } - final Intent intent = getIntent(); - final ActionBar ab = getActionBar(); - if (intent != null && intent.getBooleanExtra("init",false) && ab != null) { - ab.setDisplayShowHomeEnabled(false); - ab.setDisplayHomeAsUpEnabled(false); - ab.setHomeButtonEnabled(false); - } - this.mKnownHosts = xmppConnectionService.getKnownHosts(); - this.mKnownConferenceHosts = xmppConnectionService.getKnownConferenceHosts(); - if (this.mPendingInvite != null) { - mPendingInvite.invite(); - this.mPendingInvite = null; - } else if (!handleIntent(getIntent())) { - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } else { - filter(null); - } - } - setIntent(null); - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - Invite getInviteJellyBean(NdefRecord record) { - return new Invite(record.toUri()); - } - - protected boolean handleIntent(Intent intent) { - if (intent == null || intent.getAction() == null) { - return false; - } - switch (intent.getAction()) { - case Intent.ACTION_SENDTO: - case Intent.ACTION_VIEW: - Log.d(Config.LOGTAG, "received uri=" + intent.getData()); - return new Invite(intent.getData()).invite(); - case NfcAdapter.ACTION_NDEF_DISCOVERED: - for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) { - if (message instanceof NdefMessage) { - Log.d(Config.LOGTAG, "received message=" + message); - for (NdefRecord record : ((NdefMessage) message).getRecords()) { - switch (record.getTnf()) { - case NdefRecord.TNF_WELL_KNOWN: - if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return getInviteJellyBean(record).invite(); - } else { - byte[] payload = record.getPayload(); - if (payload[0] == 0) { - return new Invite(Uri.parse(new String(Arrays.copyOfRange( - payload, 1, payload.length)))).invite(); - } - } - } - } - } - } - } - } - return false; - } - - private boolean handleJid(Invite invite) { - List contacts = xmppConnectionService.findContacts(invite.getJid()); - if (contacts.size() == 0) { - showCreateContactDialog(invite.getJid().toString(),invite.getFingerprint()); - return false; - } else if (contacts.size() == 1) { - Contact contact = contacts.get(0); - if (invite.getFingerprint() != null) { - if (contact.addOtrFingerprint(invite.getFingerprint())) { - Log.d(Config.LOGTAG,"added new fingerprint"); - xmppConnectionService.syncRosterToDisk(contact.getAccount()); - } - } - switchToConversation(contact); - return true; - } else { - if (mMenuSearchView != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.setText(""); - mSearchEditText.append(invite.getJid().toString()); - filter(invite.getJid().toString()); - } else { - mInitialJid = invite.getJid().toString(); - } - 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.State.DISABLED) { - for (Contact contact : account.getRoster().getContacts()) { - if (contact.showInRoster() && contact.match(needle) - && (!this.mHideOfflineContacts - || contact.getPresences().getMostAvailableStatus() < Presences.OFFLINE)) { - 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.State.DISABLED) { - for (Bookmark bookmark : account.getBookmarks()) { - if (bookmark.match(needle)) { - this.conferences.add(bookmark); - } - } - } - } - Collections.sort(this.conferences); - mConferenceAdapter.notifyDataSetChanged(); - } - - private void onTabChanged() { - invalidateOptionsMenu(); - } - - @Override - public void OnUpdateBlocklist(final Status status) { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } - } - - public static class MyListFragment extends ListFragment { - private AdapterView.OnItemClickListener mOnItemClickListener; - private int mResContextMenu; - - public void setContextMenu(final int res) { - this.mResContextMenu = res; - } - - @Override - public void onListItemClick(final ListView l, final View v, final int position, final long id) { - if (mOnItemClickListener != null) { - mOnItemClickListener.onItemClick(l, v, position, id); - } - } - - public void setOnListItemClickListener(AdapterView.OnItemClickListener l) { - this.mOnItemClickListener = l; - } - - @Override - public void onViewCreated(final View view, final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - registerForContextMenu(getListView()); - getListView().setFastScrollEnabled(true); - } - - @Override - public void onCreateContextMenu(final ContextMenu menu, final View v, - final ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - final StartConversationActivity activity = (StartConversationActivity) getActivity(); - activity.getMenuInflater().inflate(mResContextMenu, menu); - final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; - if (mResContextMenu == R.menu.conference_context) { - activity.conference_context_id = acmi.position; - } else { - activity.contact_context_id = acmi.position; - final Blockable contact = (Contact) activity.contacts.get(acmi.position); - - final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock); - if (blockUnblockItem != null) { - if (contact.isBlocked()) { - blockUnblockItem.setTitle(R.string.unblock_contact); - } else { - blockUnblockItem.setTitle(R.string.block_contact); - } - } - } - } - - @Override - public boolean onContextItemSelected(final 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_contact_block_unblock: - activity.toggleContactBlock(); - 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; - } - } - - private class Invite extends XmppUri { - - public Invite(final Uri uri) { - super(uri); - } - - public Invite(final String uri) { - super(uri); - } - - boolean invite() { - if (jid != null) { - if (muc) { - showJoinConferenceDialog(jid); - } else { - return handleJid(this); - } - } - return false; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/TimePreference.java b/src/main/java/eu/siacs/conversations/ui/TimePreference.java deleted file mode 100644 index e32b068c..00000000 --- a/src/main/java/eu/siacs/conversations/ui/TimePreference.java +++ /dev/null @@ -1,105 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.preference.DialogPreference; -import android.preference.Preference; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TimePicker; - -import java.text.DateFormat; -import java.util.Calendar; -import java.util.Date; - -public class TimePreference extends DialogPreference implements Preference.OnPreferenceChangeListener { - private TimePicker picker = null; - public final static long DEFAULT_VALUE = 0; - - public TimePreference(final Context context, final AttributeSet attrs) { - super(context, attrs, 0); - this.setOnPreferenceChangeListener(this); - } - - protected void setTime(final long time) { - persistLong(time); - notifyDependencyChange(shouldDisableDependents()); - notifyChanged(); - } - - protected void updateSummary(final long time) { - final DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(getContext()); - final Date date = new Date(time); - setSummary(dateFormat.format(date.getTime())); - } - - @Override - protected View onCreateDialogView() { - picker = new TimePicker(getContext()); - picker.setIs24HourView(android.text.format.DateFormat.is24HourFormat(getContext())); - return picker; - } - - protected Calendar getPersistedTime() { - final Calendar c = Calendar.getInstance(); - c.setTimeInMillis(getPersistedLong(DEFAULT_VALUE)); - - return c; - } - - @SuppressWarnings("NullableProblems") - @Override - protected void onBindDialogView(final View v) { - super.onBindDialogView(v); - final Calendar c = getPersistedTime(); - - picker.setCurrentHour(c.get(Calendar.HOUR_OF_DAY)); - picker.setCurrentMinute(c.get(Calendar.MINUTE)); - } - - @Override - protected void onDialogClosed(final boolean positiveResult) { - super.onDialogClosed(positiveResult); - - if (positiveResult) { - final Calendar c = Calendar.getInstance(); - c.set(Calendar.MINUTE, picker.getCurrentMinute()); - c.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour()); - - - if (!callChangeListener(c.getTimeInMillis())) { - return; - } - - setTime(c.getTimeInMillis()); - } - } - - @Override - protected Object onGetDefaultValue(final TypedArray a, final int index) { - return a.getInteger(index, 0); - } - - @Override - protected void onSetInitialValue(final boolean restorePersistedValue, final Object defaultValue) { - long time; - if (defaultValue == null) { - time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE; - } else if (defaultValue instanceof Long) { - time = restorePersistedValue ? getPersistedLong((Long) defaultValue) : (Long) defaultValue; - } else if (defaultValue instanceof Calendar) { - time = restorePersistedValue ? getPersistedLong(((Calendar)defaultValue).getTimeInMillis()) : ((Calendar)defaultValue).getTimeInMillis(); - } else { - time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE; - } - - setTime(time); - updateSummary(time); - } - - @Override - public boolean onPreferenceChange(final Preference preference, final Object newValue) { - ((TimePreference) preference).updateSummary((Long)newValue); - return true; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java deleted file mode 100644 index c80199e1..00000000 --- a/src/main/java/eu/siacs/conversations/ui/UiCallback.java +++ /dev/null @@ -1,11 +0,0 @@ -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/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java deleted file mode 100644 index ec9d59e1..00000000 --- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java +++ /dev/null @@ -1,446 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.ActionBar; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.view.Menu; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import com.google.zxing.integration.android.IntentIntegrator; -import com.google.zxing.integration.android.IntentResult; - -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { - - public static final String ACTION_VERIFY_CONTACT = "verify_contact"; - public static final int MODE_SCAN_FINGERPRINT = - 0x0502; - public static final int MODE_ASK_QUESTION = 0x0503; - public static final int MODE_ANSWER_QUESTION = 0x0504; - public static final int MODE_MANUAL_VERIFICATION = 0x0505; - - private LinearLayout mManualVerificationArea; - private LinearLayout mSmpVerificationArea; - private TextView mRemoteFingerprint; - private TextView mYourFingerprint; - private TextView mVerificationExplain; - private TextView mStatusMessage; - private TextView mSharedSecretHint; - private EditText mSharedSecretHintEditable; - private EditText mSharedSecretSecret; - private Button mLeftButton; - private Button mRightButton; - private Account mAccount; - private Conversation mConversation; - private int mode = MODE_MANUAL_VERIFICATION; - private XmppUri mPendingUri = null; - - private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialogInterface, int click) { - mConversation.verifyOtrFingerprint(); - xmppConnectionService.syncRosterToDisk(mConversation.getAccount()); - Toast.makeText(VerifyOTRActivity.this,R.string.verified,Toast.LENGTH_SHORT).show(); - finish(); - } - }; - - private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(final View view) { - if (isAccountOnline()) { - final String question = mSharedSecretHintEditable.getText().toString(); - final String secret = mSharedSecretSecret.getText().toString(); - if (question.trim().isEmpty()) { - mSharedSecretHintEditable.requestFocus(); - mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty)); - } else if (secret.trim().isEmpty()) { - mSharedSecretSecret.requestFocus(); - mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty)); - } else { - mSharedSecretSecret.setError(null); - mSharedSecretHintEditable.setError(null); - initSmp(question, secret); - updateView(); - } - } - } - }; - private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (isAccountOnline()) { - abortSmp(); - updateView(); - } - } - }; - private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() { - - @Override - public void onClick(View view) { - if (isAccountOnline()) { - final String question = mSharedSecretHintEditable.getText().toString(); - final String secret = mSharedSecretSecret.getText().toString(); - respondSmp(question, secret); - updateView(); - } - } - }; - private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - mConversation.smp().hint = null; - mConversation.smp().secret = null; - updateView(); - } - }; - private View.OnClickListener mFinishListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - finish(); - } - }; - - protected boolean initSmp(final String question, final String secret) { - final Session session = mConversation.getOtrSession(); - if (session!=null) { - try { - session.initSmp(question, secret); - mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED; - mConversation.smp().secret = secret; - mConversation.smp().hint = question; - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean abortSmp() { - final Session session = mConversation.getOtrSession(); - if (session!=null) { - try { - session.abortSmp(); - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - mConversation.smp().hint = null; - mConversation.smp().secret = null; - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean respondSmp(final String question, final String secret) { - final Session session = mConversation.getOtrSession(); - if (session!=null) { - try { - session.respondSmp(question,secret); - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean verifyWithUri(XmppUri uri) { - Contact contact = mConversation.getContact(); - if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) { - contact.addOtrFingerprint(uri.getFingerprint()); - Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show(); - updateView(); - xmppConnectionService.syncRosterToDisk(contact.getAccount()); - return true; - } else { - Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show(); - return false; - } - } - - protected boolean isAccountOnline() { - if (this.mAccount.getStatus() != Account.State.ONLINE) { - Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_SHORT).show(); - return false; - } else { - return true; - } - } - - protected boolean handleIntent(Intent intent) { - if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) { - try { - this.mAccount = this.xmppConnectionService.findAccountByJid(Jid.fromString(intent.getExtras().getString("account"))); - } catch (final InvalidJidException ignored) { - return false; - } - try { - this.mConversation = this.xmppConnectionService.find(this.mAccount,Jid.fromString(intent.getExtras().getString("contact"))); - if (this.mConversation == null) { - return false; - } - } catch (final InvalidJidException ignored) { - return false; - } - this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION); - if (this.mode == MODE_SCAN_FINGERPRINT) { - new IntentIntegrator(this).initiateScan(); - return false; - } - return true; - } else { - return false; - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); - if (scanResult != null && scanResult.getFormatName() != null) { - String data = scanResult.getContents(); - XmppUri uri = new XmppUri(data); - if (xmppConnectionServiceBound) { - verifyWithUri(uri); - finish(); - } else { - this.mPendingUri = uri; - } - } else { - finish(); - } - } - super.onActivityResult(requestCode, requestCode, intent); - } - - @Override - protected void onBackendConnected() { - if (handleIntent(getIntent())) { - updateView(); - } else if (mPendingUri!=null) { - verifyWithUri(mPendingUri); - finish(); - mPendingUri = null; - } - setIntent(null); - } - - protected void updateView() { - if (this.mConversation.hasValidOtrSession()) { - final ActionBar actionBar = getActionBar(); - this.mVerificationExplain.setText(R.string.no_otr_session_found); - invalidateOptionsMenu(); - switch(this.mode) { - case MODE_ASK_QUESTION: - if (actionBar != null ) { - actionBar.setTitle(R.string.ask_question); - } - this.updateViewAskQuestion(); - break; - case MODE_ANSWER_QUESTION: - if (actionBar != null ) { - actionBar.setTitle(R.string.smp_requested); - } - this.updateViewAnswerQuestion(); - break; - case MODE_MANUAL_VERIFICATION: - default: - if (actionBar != null ) { - actionBar.setTitle(R.string.manually_verify); - } - this.updateViewManualVerification(); - break; - } - } else { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.GONE); - } - } - - protected void updateViewManualVerification() { - this.mVerificationExplain.setText(R.string.manual_verification_explanation); - this.mManualVerificationArea.setVisibility(View.VISIBLE); - this.mSmpVerificationArea.setVisibility(View.GONE); - this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); - this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint())); - if (this.mConversation.isOtrFingerprintVerified()) { - deactivateButton(this.mRightButton,R.string.verified); - activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); - } else { - activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); - activateButton(this.mRightButton,R.string.verify, new View.OnClickListener() { - @Override - public void onClick(View view) { - showManuallyVerifyDialog(); - } - }); - } - } - - protected void updateViewAskQuestion() { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.VISIBLE); - this.mVerificationExplain.setText(R.string.smp_explain_question); - final int smpStatus = this.mConversation.smp().status; - switch (smpStatus) { - case Conversation.Smp.STATUS_WE_REQUESTED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint); - this.mSharedSecretSecret.setText(this.mConversation.smp().secret); - this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener); - this.deactivateButton(this.mRightButton, R.string.in_progress); - break; - case Conversation.Smp.STATUS_FAILED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.requestFocus(); - this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); - this.deactivateButton(this.mLeftButton, R.string.cancel); - this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener); - break; - case Conversation.Smp.STATUS_VERIFIED: - this.mSharedSecretHintEditable.setText(""); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretSecret.setText(""); - this.mSharedSecretSecret.setVisibility(View.GONE); - this.mStatusMessage.setVisibility(View.VISIBLE); - this.deactivateButton(this.mLeftButton, R.string.cancel); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - default: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); - this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener); - break; - } - } - - protected void updateViewAnswerQuestion() { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.VISIBLE); - this.mVerificationExplain.setText(R.string.smp_explain_answer); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretHint.setVisibility(View.VISIBLE); - this.deactivateButton(this.mLeftButton, R.string.cancel); - final int smpStatus = this.mConversation.smp().status; - switch (smpStatus) { - case Conversation.Smp.STATUS_CONTACT_REQUESTED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHint.setText(this.mConversation.smp().hint); - this.activateButton(this.mRightButton,R.string.respond,this.mRespondSharedSecretListener); - break; - case Conversation.Smp.STATUS_VERIFIED: - this.mSharedSecretHintEditable.setText(""); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretHint.setVisibility(View.GONE); - this.mSharedSecretSecret.setText(""); - this.mSharedSecretSecret.setVisibility(View.GONE); - this.mStatusMessage.setVisibility(View.VISIBLE); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - case Conversation.Smp.STATUS_FAILED: - default: - this.mSharedSecretSecret.requestFocus(); - this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); - this.activateButton(this.mRightButton,R.string.finish,this.mFinishListener); - break; - } - } - - protected void activateButton(Button button, int text, View.OnClickListener listener) { - button.setEnabled(true); - button.setTextColor(getPrimaryTextColor()); - button.setText(text); - button.setOnClickListener(listener); - } - - protected void deactivateButton(Button button, int text) { - button.setEnabled(false); - button.setTextColor(getSecondaryTextColor()); - button.setText(text); - button.setOnClickListener(null); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_verify_otr); - this.mRemoteFingerprint = (TextView) findViewById(R.id.remote_fingerprint); - this.mYourFingerprint = (TextView) findViewById(R.id.your_fingerprint); - this.mLeftButton = (Button) findViewById(R.id.left_button); - this.mRightButton = (Button) findViewById(R.id.right_button); - this.mVerificationExplain = (TextView) findViewById(R.id.verification_explanation); - this.mStatusMessage = (TextView) findViewById(R.id.status_message); - this.mSharedSecretSecret = (EditText) findViewById(R.id.shared_secret_secret); - this.mSharedSecretHintEditable = (EditText) findViewById(R.id.shared_secret_hint_editable); - this.mSharedSecretHint = (TextView) findViewById(R.id.shared_secret_hint); - this.mManualVerificationArea = (LinearLayout) findViewById(R.id.manual_verification_area); - this.mSmpVerificationArea = (LinearLayout) findViewById(R.id.smp_verification_area); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.verify_otr, menu); - return true; - } - - private void showManuallyVerifyDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.manually_verify); - builder.setMessage(R.string.are_you_sure_verify_fingerprint); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener); - builder.create().show(); - } - - @Override - protected String getShareableUri() { - if (mAccount!=null) { - return mAccount.getShareableUri(); - } else { - return ""; - } - } - - public void onConversationUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - updateView(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java deleted file mode 100644 index 62f62b9a..00000000 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ /dev/null @@ -1,957 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.ActionBar; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; -import android.app.PendingIntent; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.content.IntentSender.SendIntentException; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.nfc.NfcEvent; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.SystemClock; -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; -import android.widget.Toast; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.WriterException; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; - -import net.java.otr4j.session.SessionID; - -import java.io.FileNotFoundException; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import de.tzur.conversations.Settings; -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.MucOptions; -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 eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -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 registeredListeners = 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 int mTheme; - protected boolean mUsingEnterKey = false; - - private long mLastUiRefresh = 0; - private Handler mRefreshUiHandler = new Handler(); - private Runnable mRefreshUiRunnable = new Runnable() { - @Override - public void run() { - mLastUiRefresh = SystemClock.elapsedRealtime(); - refreshUiReal(); - } - }; - - - protected void refreshUi() { - final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; - if (diff > Config.REFRESH_UI_INTERVAL) { - mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); - runOnUiThread(mRefreshUiRunnable); - } else { - final long next = Config.REFRESH_UI_INTERVAL - diff; - mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); - mRefreshUiHandler.postDelayed(mRefreshUiRunnable,next); - } - } - - protected void refreshUiReal() { - - }; - - 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; - if (!registeredListeners && shouldRegisterListeners()) { - registerListeners(); - registeredListeners = true; - } - onBackendConnected(); - } - - @Override - public void onServiceDisconnected(ComponentName arg0) { - xmppConnectionServiceBound = false; - } - }; - - @Override - protected void onStart() { - super.onStart(); - if (!xmppConnectionServiceBound) { - connectToBackend(); - } else { - if (!registeredListeners) { - this.registerListeners(); - this.registeredListeners = true; - } - this.onBackendConnected(); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - protected boolean shouldRegisterListeners() { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return !isDestroyed() && !isFinishing(); - } else { - return !isFinishing(); - } - } - - 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) { - if (registeredListeners) { - this.unregisterListeners(); - this.registeredListeners = false; - } - 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(); - - protected void registerListeners() { - if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); - } - if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); - } - if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); - } - if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); - } - if (this instanceof OnUpdateBlocklist) { - this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this); - } - } - - protected void unregisterListeners() { - if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.removeOnConversationListChangedListener(); - } - if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.removeOnAccountListChangedListener(); - } - if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.removeOnRosterUpdateListener(); - } - if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.removeOnMucRosterUpdateListener(); - } - if (this instanceof OnUpdateBlocklist) { - this.xmppConnectionService.removeOnUpdateBlocklistListener(); - } - } - - @Override - public boolean onOptionsItemSelected(final 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; - case R.id.action_show_qr_code: - showQrCode(); - 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); - this.mTheme = findTheme(); - setTheme(this.mTheme); - this.mUsingEnterKey = usingEnterKey(); - mUseSubject = getPreferences().getBoolean("use_subject", true); - - Settings.initSettingsClassWithPreferences(getPreferences()); - - final ActionBar ab = getActionBar(); - if (ab!=null) { - ab.setDisplayHomeAsUpEnabled(true); - } - } - - protected boolean usingEnterKey() { - return getPreferences().getBoolean("display_enter_key", false); - } - - 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) { - switchToConversation(conversation,text,null,newTask); - } - - public void highlightInMuc(Conversation conversation, String nick) { - switchToConversation(conversation,null,nick,false); - } - - private void switchToConversation(Conversation conversation, String text, String nick, 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); - } - if (nick != null) { - viewConversationIntent.putExtra(ConversationActivity.NICK, nick); - } - 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().toBareJid().toString()); - intent.putExtra("contact", contact.getJid().toString()); - startActivity(intent); - } - - public void switchToAccount(Account account) { - Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().toBareJid().toString()); - startActivity(intent); - } - - protected void inviteToConversation(Conversation conversation) { - Intent intent = new Intent(getApplicationContext(), - ChooseContactActivity.class); - List contacts = new ArrayList<>(); - if (conversation.getMode() == Conversation.MODE_MULTI) { - for (MucOptions.User user : conversation.getMucOptions().getUsers()) { - Jid jid = user.getJid(); - if (jid != null) { - contacts.add(jid.toBareJid().toString()); - } - } - } else { - contacts.add(conversation.getJid().toBareJid().toString()); - } - intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()])); - intent.putExtra("conversation", conversation.getUuid()); - intent.putExtra("multiple", true); - 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 (final SendIntentException ignored) { - } - } - - @Override - public void success(Account account) { - xmppConnectionService.databaseBackend - .updateAccount(account); - xmppConnectionService.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) { - showAddToRosterDialog(conversation.getContact()); - } - - protected void showAddToRosterDialog(final Contact contact) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(contact.getJid().toString()); - builder.setMessage(getString(R.string.not_in_roster)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add_contact), - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - final Jid jid = contact.getJid(); - Account account = contact.getAccount(); - Contact contact = account.getRoster().getContact(jid); - xmppConnectionService.createContact(contact); - } - }); - builder.create().show(); - } - - private void showAskForPresenceDialog(final Contact contact) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(contact.getJid().toString()); - 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().toString()); - 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.setNextCounterpart(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 = 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) { - final Contact contact = conversation.getContact(); - if (conversation.hasValidOtrSession()) { - SessionID id = conversation.getOtrSession().getSessionID(); - Jid jid; - try { - jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID()); - } catch (InvalidJidException e) { - jid = null; - } - conversation.setNextCounterpart(jid); - listener.onPresenceSelected(); - } else 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.State.ONLINE) { - showAskForPresenceDialog(contact); - } else if (!contact.getOption(Contact.Options.TO) - || !contact.getOption(Contact.Options.FROM)) { - warnMutalPresenceSubscription(conversation, listener); - } else { - conversation.setNextCounterpart(null); - listener.onPresenceSelected(); - } - } else if (presences.size() == 1) { - String presence = presences.asStringArray()[0]; - try { - conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence)); - } catch (InvalidJidException e) { - conversation.setNextCounterpart(null); - } - 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) { - try { - conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString())); - } catch (InvalidJidException e) { - conversation.setNextCounterpart(null); - } - listener.onPresenceSelected(); - } - }); - builder.create().show(); - } - } - } - - protected void onActivityResult(int requestCode, int resultCode, - final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_INVITE_TO_CONVERSATION - && resultCode == RESULT_OK) { - try { - String conversationUuid = data.getStringExtra("conversation"); - Conversation conversation = xmppConnectionService - .findConversationByUuid(conversationUuid); - List jids = new ArrayList(); - if (data.getBooleanExtra("multiple", false)) { - String[] toAdd = data.getStringArrayExtra("contacts"); - for (String item : toAdd) { - jids.add(Jid.fromString(item)); - } - } else { - jids.add(Jid.fromString(data.getStringExtra("contact"))); - } - - if (conversation.getMode() == Conversation.MODE_MULTI) { - for (Jid jid : jids) { - xmppConnectionService.invite(conversation, jid); - } - } else { - jids.add(conversation.getJid().toBareJid()); - xmppConnectionService.createAdhocConference(conversation.getAccount(), jids, adhocCallback); - } - } catch (final InvalidJidException ignored) { - - } - } - } - - private UiCallback adhocCallback = new UiCallback() { - @Override - public void success(final Conversation conversation) { - switchToConversation(conversation); - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show(); - } - }); - } - - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show(); - } - }); - } - - @Override - public void userInputRequried(PendingIntent pi, Conversation object) { - - } - }; - - 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 getOnlineColor() { - return this.mColorGreen; - } - - public int getSecondaryBackgroundColor() { - return this.mSecondaryBackgroundColor; - } - - public int getPixel(int dp) { - DisplayMetrics metrics = getResources().getDisplayMetrics(); - return ((int) (dp * metrics.density)); - } - - public boolean copyTextToClipboard(String text, int labelResId) { - ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - String label = getResources().getString(labelResId); - if (mClipBoardManager != null) { - ClipData mClipData = ClipData.newPlainText(label, text); - mClipBoardManager.setPrimaryClip(mClipData); - return true; - } - return false; - } - - protected void registerNdefPushMessageCallback() { - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); - if (nfcAdapter != null && nfcAdapter.isEnabled()) { - nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { - @Override - public NdefMessage createNdefMessage(NfcEvent nfcEvent) { - return new NdefMessage(new NdefRecord[]{ - NdefRecord.createUri(getShareableUri()), - NdefRecord.createApplicationRecord("eu.siacs.conversations") - }); - } - }, this); - } - } - - protected void unregisterNdefPushMessageCallback() { - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); - if (nfcAdapter != null && nfcAdapter.isEnabled()) { - nfcAdapter.setNdefPushMessageCallback(null,this); - } - } - - protected String getShareableUri() { - return null; - } - - @Override - public void onResume() { - super.onResume(); - if (this.getShareableUri()!=null) { - this.registerNdefPushMessageCallback(); - } - } - - protected int findTheme() { - if (getPreferences().getBoolean("use_larger_font", false)) { - return R.style.ConversationsTheme_LargerText; - } else { - return R.style.ConversationsTheme; - } - } - - @Override - public void onPause() { - super.onPause(); - this.unregisterNdefPushMessageCallback(); - } - - protected void showQrCode() { - String uri = getShareableUri(); - if (uri!=null) { - Point size = new Point(); - getWindowManager().getDefaultDisplay().getSize(size); - final int width = (size.x < size.y ? size.x : size.y); - Bitmap bitmap = createQrCodeBitmap(uri, width); - ImageView view = new ImageView(this); - view.setImageBitmap(bitmap); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setView(view); - builder.create().show(); - } - } - - protected Bitmap createQrCodeBitmap(String input, int size) { - Log.d(Config.LOGTAG,"qr code requested size: "+size); - try { - final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter(); - final Hashtable hints = new Hashtable<>(); - hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); - final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints); - final int width = result.getWidth(); - final int height = result.getHeight(); - final int[] pixels = new int[width * height]; - for (int y = 0; y < height; y++) { - final int offset = y * width; - for (int x = 0; x < width; x++) { - pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT; - } - } - final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Log.d(Config.LOGTAG,"output size: "+width+"x"+height); - bitmap.setPixels(pixels, 0, width, 0, 0, width, height); - return bitmap; - } catch (final WriterException e) { - return null; - } - } - - 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 (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 (final RejectedExecutionException ignored) { - } - } - } - } - - 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/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java deleted file mode 100644 index 29730914..00000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ /dev/null @@ -1,54 +0,0 @@ -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 = inflater.inflate(R.layout.account_row, parent, false); - } - TextView jid = (TextView) view.findViewById(R.id.account_jid); - jid.setText(account.getJid().toBareJid().toString()); - 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))); - statusView.setText(getContext().getString(account.getStatus().getReadableId())); - switch (account.getStatus()) { - case ONLINE: - statusView.setTextColor(activity.getOnlineColor()); - break; - case DISABLED: - case CONNECTING: - statusView.setTextColor(activity.getSecondaryTextColor()); - break; - default: - statusView.setTextColor(activity.getWarningTextColor()); - break; - } - return view; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java deleted file mode 100644 index b6f88356..00000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ /dev/null @@ -1,237 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import java.lang.ref.WeakReference; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import de.tzur.conversations.Settings; -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.Downloadable; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presences; -import eu.siacs.conversations.ui.ConversationActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.utils.UIHelper; -import github.ankushsachdeva.emojicon.EmojiconTextView; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.Typeface; -import android.preference.PreferenceManager; -import android.util.Log; -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 = 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.getJid().toBareJid().toString()); - } - EmojiconTextView mLastMessage = (EmojiconTextView) view.findViewById(R.id.conversation_lastmsg); - TextView mTimestamp = (TextView) view.findViewById(R.id.conversation_lastupdate); - ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage); - - if (Settings.SHOW_ONLINE_STATUS && conversation != null && conversation.getAccount().getStatus() == Account.State.ONLINE) { - TextView status = (TextView) view.findViewById(R.id.status); - - String color = "#000000"; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - switch (conversation.getContact().getMostAvailableStatus()) { - case Presences.ONLINE: - case Presences.CHAT: - color = "#259B23"; - break; - case Presences.AWAY: - case Presences.XA: - color = "#FF9800"; - break; - case Presences.DND: - color = "#E51C23"; - break; - } - } else if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().online()) { - color = "#259B23"; - } - status.setBackgroundColor(Color.parseColor(color)); - } - - Message message = conversation.getLatestMessage(); - - if (!conversation.isRead()) { - convName.setTypeface(null, Typeface.BOLD); - } else { - convName.setTypeface(null, Typeface.NORMAL); - } - - if (message.getImageParams().width > 0 - && (message.getDownloadable() == null - || message.getDownloadable().getStatus() != Downloadable.STATUS_DELETED)) { - mLastMessage.setVisibility(View.GONE); - imagePreview.setVisibility(View.VISIBLE); - activity.loadBitmap(message, imagePreview); - } else { - Pair preview = UIHelper.getMessagePreview(activity,message); - mLastMessage.setVisibility(View.VISIBLE); - imagePreview.setVisibility(View.GONE); - boolean parseEmoticons = Settings.PARSE_EMOTICONS; - CharSequence msgText = parseEmoticons ? UIHelper.transformAsciiEmoticons(getContext(), preview.first) : preview.first; - mLastMessage.setText(msgText); - if (preview.second) { - if (conversation.isRead()) { - mLastMessage.setTypeface(null, Typeface.ITALIC); - } else { - mLastMessage.setTypeface(null,Typeface.BOLD_ITALIC); - } - } else { - if (conversation.isRead()) { - mLastMessage.setTypeface(null,Typeface.NORMAL); - } else { - mLastMessage.setTypeface(null,Typeface.BOLD); - } - } - } - - mTimestamp.setText(UIHelper.readableTimeDifference(activity,conversation.getLatestMessage().getTimeSent())); - ImageView profilePicture = (ImageView) view.findViewById(R.id.conversation_image); - loadAvatar(conversation,profilePicture); - - return view; - } - - class BitmapWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private Conversation conversation = null; - - public BitmapWorkerTask(ImageView imageView) { - imageViewReference = new WeakReference<>(imageView); - } - - @Override - protected Bitmap doInBackground(Conversation... params) { - return activity.avatarService().get(params[0], activity.getPixel(56)); - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - } - - public void loadAvatar(Conversation conversation, ImageView imageView) { - if (cancelPotentialWork(conversation, imageView)) { - final Bitmap bm = activity.avatarService().get(conversation, activity.getPixel(56), true); - if (bm != null) { - imageView.setImageBitmap(bm); - imageView.setBackgroundColor(0x00000000); - } else { - imageView.setBackgroundColor(UIHelper.getColorForName(conversation.getName())); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(conversation); - } catch (final RejectedExecutionException ignored) { - } - } - } - } - - public static boolean cancelPotentialWork(Conversation conversation, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Conversation oldConversation = bitmapWorkerTask.conversation; - if (oldConversation == null || conversation != oldConversation) { - 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(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java deleted file mode 100644 index 0993735f..00000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java +++ /dev/null @@ -1,71 +0,0 @@ -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, new ArrayList()); - domains = new ArrayList(mKnownHosts); - } - - @Override - public Filter getFilter() { - return domainFilter; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java deleted file mode 100644 index 7b20b55f..00000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ /dev/null @@ -1,181 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import java.lang.ref.WeakReference; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.jid.Jid; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.preference.PreferenceManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -public class ListItemAdapter extends ArrayAdapter { - - protected XmppActivity activity; - protected boolean showDynamicTags = false; - private View.OnClickListener onTagTvClick = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (view instanceof TextView && mOnTagClickedListener != null) { - TextView tv = (TextView) view; - final String tag = tv.getText().toString(); - mOnTagClickedListener.onTagClicked(tag); - } - } - }; - private OnTagClickedListener mOnTagClickedListener = null; - - public ListItemAdapter(XmppActivity activity, List objects) { - super(activity, 0, objects); - this.activity = activity; - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false); - } - - @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 = inflater.inflate(R.layout.contact, parent, false); - } - TextView tvName = (TextView) view.findViewById(R.id.contact_display_name); - TextView tvJid = (TextView) view.findViewById(R.id.contact_jid); - ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); - LinearLayout tagLayout = (LinearLayout) view.findViewById(R.id.tags); - - List tags = item.getTags(); - if (tags.size() == 0 || !this.showDynamicTags) { - tagLayout.setVisibility(View.GONE); - } else { - tagLayout.setVisibility(View.VISIBLE); - tagLayout.removeAllViewsInLayout(); - for(ListItem.Tag tag : tags) { - TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tagLayout,false); - tv.setText(tag.getName()); - tv.setBackgroundColor(tag.getColor()); - tv.setOnClickListener(this.onTagTvClick); - tagLayout.addView(tv); - } - } - final Jid jid = item.getJid(); - if (jid != null) { - tvJid.setText(jid.toString()); - } else { - tvJid.setText(""); - } - tvName.setText(item.getDisplayName()); - loadAvatar(item,picture); - return view; - } - - public void setOnTagClickedListener(OnTagClickedListener listener) { - this.mOnTagClickedListener = listener; - } - - public interface OnTagClickedListener { - public void onTagClicked(String tag); - } - - class BitmapWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private ListItem item = null; - - public BitmapWorkerTask(ImageView imageView) { - imageViewReference = new WeakReference<>(imageView); - } - - @Override - protected Bitmap doInBackground(ListItem... params) { - return activity.avatarService().get(params[0], activity.getPixel(48)); - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - } - - public void loadAvatar(ListItem item, ImageView imageView) { - if (cancelPotentialWork(item, imageView)) { - final Bitmap bm = activity.avatarService().get(item,activity.getPixel(48),true); - if (bm != null) { - imageView.setImageBitmap(bm); - imageView.setBackgroundColor(0x00000000); - } else { - imageView.setBackgroundColor(UIHelper.getColorForName(item.getDisplayName())); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(item); - } catch (final RejectedExecutionException ignored) { - } - } - } - } - - public static boolean cancelPotentialWork(ListItem item, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final ListItem oldItem = bitmapWorkerTask.item; - if (oldItem == null || item != oldItem) { - 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/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java deleted file mode 100644 index 933ca1f8..00000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ /dev/null @@ -1,604 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.Typeface; -import android.net.Uri; -import android.preference.PreferenceManager; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnLongClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.List; - -import de.tzur.conversations.Settings; -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.Downloadable; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Message.ImageParams; -import eu.siacs.conversations.ui.ConversationActivity; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.UIHelper; -import github.ankushsachdeva.emojicon.EmojiconTextView; - -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; - - private OnLongClickListener openContextMenu = new OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - v.showContextMenu(); - return true; - } - }; - - 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.getType() == Message.TYPE_FILE || message.getDownloadable() != null) { - ImageParams params = message.getImageParams(); - if (params.size > (1.5 * 1024 * 1024)) { - filesize = params.size / (1024 * 1024)+ " MiB"; - } else if (params.size > 0) { - filesize = params.size / 1024 + " KiB"; - } - if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) { - error = true; - } - } - switch (message.getMergedStatus()) { - case Message.STATUS_WAITING: - info = getContext().getString(R.string.waiting); - break; - case Message.STATUS_UNSEND: - Downloadable d = message.getDownloadable(); - if (d!=null) { - info = getContext().getString(R.string.sending_file,d.getProgress()); - } else { - info = getContext().getString(R.string.sending); - } - break; - case Message.STATUS_OFFERED: - info = getContext().getString(R.string.offering); - break; - case Message.STATUS_SEND_RECEIVED: - 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; - default: - if (multiReceived) { - info = UIHelper.getMessageDisplayName(message); - } - 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, String text) { - if (viewHolder.download_button != null) { - viewHolder.download_button.setVisibility(View.GONE); - } - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText(text); - 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(final ViewHolder viewHolder, final 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) { - final String nick = UIHelper.getMessageDisplayName(message); - final String formattedBody = message.getMergedBody().replaceAll("^" + Message.ME_COMMAND, - nick + " "); - if (message.getType() != Message.TYPE_PRIVATE) { - - if (message.hasMeCommand()) { - final Spannable span = new SpannableString(formattedBody); - span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(span); - } else { - boolean parseEmoticons = Settings.PARSE_EMOTICONS; - viewHolder.messageBody.setText(parseEmoticons ? UIHelper - .transformAsciiEmoticons(getContext(), message.getMergedBody()) - : message.getMergedBody()); - } - } else { - String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { - privateMarker = activity - .getString(R.string.private_message); - } else { - final String to; - if (message.getCounterpart() != null) { - to = message.getCounterpart().getResourcepart(); - } else { - to = ""; - } - privateMarker = activity.getString(R.string.private_message_to, to); - } - final Spannable span = new SpannableString(privateMarker + " " - + formattedBody); - span.setSpan(new ForegroundColorSpan(activity - .getSecondaryTextColor()), 0, privateMarker - .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - span.setSpan(new StyleSpan(Typeface.BOLD), 0, - privateMarker.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - if (message.hasMeCommand()) { - span.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarker.length() + 1, - privateMarker.length() + 1 + nick.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, String text) { - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(text); - viewHolder.download_button.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - startDownloadable(message); - } - }); - viewHolder.download_button.setOnLongClickListener(openContextMenu); - } - - private void displayOpenableMessage(ViewHolder viewHolder,final Message message) { - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity,message))); - viewHolder.download_button.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - openDownloadable(message); - } - }); - viewHolder.download_button.setOnLongClickListener(openContextMenu); - } - - private void displayLocationMessage(ViewHolder viewHolder, final Message message) { - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(R.string.show_location); - viewHolder.download_button.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - showLocation(message); - } - }); - viewHolder.download_button.setOnLongClickListener(openContextMenu); - } - - 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(openContextMenu); - } - - @Override - public View getView(int position, View view, ViewGroup parent) { - final Message message = getItem(position); - final Conversation conversation = message.getConversation(); - final Account account = conversation.getAccount(); - final int type = getItemViewType(position); - ViewHolder viewHolder; - if (view == null) { - viewHolder = new ViewHolder(); - switch (type) { - case NULL: - view = activity.getLayoutInflater().inflate( - R.layout.message_null, parent, false); - break; - case SENT: - 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.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 = (EmojiconTextView) view - .findViewById(R.id.message_body); - viewHolder.time = (TextView) view - .findViewById(R.id.message_time); - viewHolder.indicatorReceived = (ImageView) view - .findViewById(R.id.indicator_received); - break; - case RECEIVED: - 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); - viewHolder.indicator = (ImageView) view - .findViewById(R.id.security_indicator); - viewHolder.image = (ImageView) view - .findViewById(R.id.message_image); - viewHolder.messageBody = (EmojiconTextView) view - .findViewById(R.id.message_body); - viewHolder.time = (TextView) view - .findViewById(R.id.message_time); - viewHolder.indicatorReceived = (ImageView) view - .findViewById(R.id.indicator_received); - break; - case STATUS: - view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); - viewHolder.contact_picture = (ImageView) view.findViewById(R.id.message_photo); - viewHolder.status_message = (TextView) view.findViewById(R.id.status_message); - break; - default: - viewHolder = null; - break; - } - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - if (viewHolder == null) { - return view; - } - } - - if (type == STATUS) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - viewHolder.contact_picture.setImageBitmap(activity - .avatarService().get(conversation.getContact(), - activity.getPixel(32))); - viewHolder.contact_picture.setAlpha(0.5f); - viewHolder.status_message.setText(message.getBody()); - } - return view; - } else if (type == NULL) { - if (viewHolder.message_box != null) { - Log.e(Config.LOGTAG, "detected type=NULL but with wrong cached view"); - view = activity.getLayoutInflater().inflate(R.layout.message_null, parent, false); - view.setTag(new ViewHolder()); - } - if (position == getCount() - 1) { - view.getLayoutParams().height = 1; - } else { - view.getLayoutParams().height = 0; - - } - view.setLayoutParams(view.getLayoutParams()); - return view; - } else if (message.wasMergedIntoPrevious()) { - Log.e(Config.LOGTAG,"detected wasMergedIntoPrevious with wrong type"); - return view; - } else if (viewHolder.messageBody == null || viewHolder.image == null) { - return view; //avoiding weird platform bugs - } else if (type == RECEIVED) { - Contact contact = message.getContact(); - if (contact != null) { - viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(contact, activity.getPixel(48))); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - viewHolder.contact_picture.setImageBitmap(activity.avatarService().get( - UIHelper.getMessageDisplayName(message), - activity.getPixel(48))); - } - } else if (type == SENT) { - viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48))); - } - - viewHolder.contact_picture - .setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - if (MessageAdapter.this.mOnContactPictureClickedListener != null) { - MessageAdapter.this.mOnContactPictureClickedListener - .onContactPictureClicked(message); - } - - } - }); - viewHolder.contact_picture - .setOnLongClickListener(new OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { - MessageAdapter.this.mOnContactPictureLongClickedListener - .onContactPictureLongClicked(message); - return true; - } else { - return false; - } - } - }); - - final Downloadable downloadable = message.getDownloadable(); - if (downloadable != null && downloadable.getStatus() != Downloadable.STATUS_UPLOADING) { - if (downloadable.getStatus() == Downloadable.STATUS_OFFER) { - displayDownloadableMessage(viewHolder,message,activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message))); - } else if (downloadable.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_image_filesize)); - } else { - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first); - } - } else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { - displayImageMessage(viewHolder, message); - } else if (message.getType() == Message.TYPE_FILE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { - if (message.getImageParams().width > 0) { - displayImageMessage(viewHolder,message); - } else { - displayOpenableMessage(viewHolder, message); - } - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - if (activity.hasPgp()) { - displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message)); - } else { - displayInfoMessage(viewHolder, - activity.getString(R.string.install_openkeychain)); - if (viewHolder != null) { - viewHolder.message_box - .setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - activity.showInstallPgpDialog(); - } - }); - } - } - } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayDecryptionFailed(viewHolder); - } else { - if (GeoHelper.isGeoUri(message.getBody())) { - displayLocationMessage(viewHolder,message); - } else { - displayTextMessage(viewHolder, message); - } - } - - displayStatus(viewHolder, message); - - return view; - } - - public void startDownloadable(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(); - } - } - } - - public void openDownloadable(Message message) { - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - if (!file.exists()) { - Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); - return; - } - Intent openIntent = new Intent(Intent.ACTION_VIEW); - openIntent.setDataAndType(Uri.fromFile(file), file.getMimeType()); - PackageManager manager = activity.getPackageManager(); - List infos = manager.queryIntentActivities(openIntent, 0); - if (infos.size() > 0) { - getContext().startActivity(openIntent); - } else { - Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show(); - } - } - - public void showLocation(Message message) { - for(Intent intent : GeoHelper.createGeoIntentsFromMessage(message)) { - if (intent.resolveActivity(getContext().getPackageManager()) != null) { - getContext().startActivity(intent); - return; - } - } - Toast.makeText(activity,R.string.no_application_found_to_display_location,Toast.LENGTH_SHORT).show(); - } - - public interface OnContactPictureClicked { - public void onContactPictureClicked(Message message); - } - - public interface OnContactPictureLongClicked { - public void onContactPictureLongClicked(Message message); - } - - 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 EmojiconTextView messageBody; - protected ImageView contact_picture; - protected TextView status_message; - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java deleted file mode 100644 index eb7e2c3c..00000000 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ /dev/null @@ -1,124 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.security.SecureRandom; -import java.text.Normalizer; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; - -import eu.siacs.conversations.Config; - -public final class CryptoHelper { - public static final String FILETRANSFER = "?FILETRANSFERv1:"; - private final static char[] hexArray = "0123456789abcdef".toCharArray(); - private final static char[] vowels = "aeiou".toCharArray(); - private final static char[] consonants = "bcdfghjklmnpqrstvwxyz".toCharArray(); - final public static byte[] ONE = new byte[] { 0, 0, 0, 1 }; - - 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 hexToString(final String hexString) { - return new String(hexToBytes(hexString)); - } - - public 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 randomMucName(SecureRandom random) { - return randomWord(3, random) + "." + randomWord(7, random); - } - - private 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(); - } - - /** - * Escapes usernames or passwords for SASL. - */ - public static String saslEscape(final String s) { - final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1)); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case ',': - sb.append("=2C"); - break; - case '=': - sb.append("=3D"); - break; - default: - sb.append(c); - break; - } - } - return sb.toString(); - } - - public static String saslPrep(final String s) { - return Normalizer.normalize(s, Normalizer.Form.NFKC); - } - - public static String prettifyFingerprint(String fingerprint) { - StringBuilder builder = new StringBuilder(fingerprint); - builder.insert(8, " "); - builder.insert(17, " "); - builder.insert(26, " "); - builder.insert(35, " "); - return builder.toString(); - } - - public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) { - final Collection cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS)); - final List platformCiphers = Arrays.asList(platformSupportedCipherSuites); - cipherSuites.retainAll(platformCiphers); - cipherSuites.addAll(platformCiphers); - filterWeakCipherSuites(cipherSuites); - return cipherSuites.toArray(new String[cipherSuites.size()]); - } - - private static void filterWeakCipherSuites(final Collection cipherSuites) { - final Iterator it = cipherSuites.iterator(); - while (it.hasNext()) { - String cipherName = it.next(); - // remove all ciphers with no or very weak encryption or no authentication - for (String weakCipherPattern : Config.WEAK_CIPHER_PATTERNS) { - if (cipherName.contains(weakCipherPattern)) { - it.remove(); - break; - } - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java deleted file mode 100644 index bcb2ca44..00000000 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ /dev/null @@ -1,183 +0,0 @@ -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 eu.siacs.conversations.xmpp.jid.Jid; - -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(final Jid jid) throws IOException { - final String host = jid.getDomainpart(); - String dns[] = client.findDNS(); - - if (dns != null) { - for (String dnsserver : dns) { - InetAddress ip = InetAddress.getByName(dnsserver); - Bundle b = queryDNS(host, ip); - if (b.containsKey("values")) { - 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 bundle = 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) { - bundle.putString("error", "nosrv"); - return bundle; - } - ArrayList values = new ArrayList<>(); - for (SRV srv : result) { - if (ips6.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6)); - } - if (ips4.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4)); - } - values.add(createNamePortBundle(srv.getName(),srv.getPort(),null)); - } - bundle.putParcelableArrayList("values", values); - } catch (SocketTimeoutException e) { - bundle.putString("error", "timeout"); - } catch (Exception e) { - bundle.putString("error", "unhandled"); - } - return bundle; - } - - private static Bundle createNamePortBundle(String name, int port, TreeMap> ips) { - Bundle namePort = new Bundle(); - namePort.putString("name", name); - namePort.putInt("port", port); - if (ips!=null) { - ArrayList ip = ips.get(name); - Collections.shuffle(ip, new Random()); - namePort.putString("ip", ip.get(0)); - } - 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/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java deleted file mode 100644 index 0ad57fe2..00000000 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -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()); - os.flush(); - os.close(); - } 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/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java deleted file mode 100644 index ee3ea3e1..00000000 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ /dev/null @@ -1,119 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.io.BufferedReader; -import java.io.FileInputStream; -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 eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -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().toBareJid() - + " to send in stack trace"); - Conversation conversation = null; - try { - conversation = service.findOrCreateConversation(finalAccount, - Jid.fromString("bugs@siacs.eu"), false); - } catch (final InvalidJidException ignored) { - } - 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) - .apply(); - } - }); - builder.create().show(); - } catch (final IOException ignored) { - } - - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java deleted file mode 100644 index ceda7293..00000000 --- a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.siacs.conversations.utils; - -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; - -public class ExifHelper { - private static final String TAG = "CameraExif"; - - public static int getOrientation(InputStream is) { - if (is == null) { - return 0; - } - - byte[] buf = new byte[8]; - int length = 0; - - // ISO/IEC 10918-1:1993(E) - while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) { - int marker = buf[1] & 0xFF; - - // Check if the marker is a padding. - if (marker == 0xFF) { - continue; - } - - // Check if the marker is SOI or TEM. - if (marker == 0xD8 || marker == 0x01) { - continue; - } - // Check if the marker is EOI or SOS. - if (marker == 0xD9 || marker == 0xDA) { - return 0; - } - - // Get the length and check if it is reasonable. - if (!read(is, buf, 2)) { - return 0; - } - length = pack(buf, 0, 2, false); - if (length < 2) { - Log.e(TAG, "Invalid length"); - return 0; - } - length -= 2; - - // Break if the marker is EXIF in APP1. - if (marker == 0xE1 && length >= 6) { - if (!read(is, buf, 6)) return 0; - length -= 6; - if (pack(buf, 0, 4, false) == 0x45786966 && - pack(buf, 4, 2, false) == 0) { - break; - } - } - - // Skip other markers. - try { - is.skip(length); - } catch (IOException ex) { - return 0; - } - length = 0; - } - - // JEITA CP-3451 Exif Version 2.2 - if (length > 8) { - int offset = 0; - byte[] jpeg = new byte[length]; - if (!read(is, jpeg, length)) { - return 0; - } - - // Identify the byte order. - int tag = pack(jpeg, offset, 4, false); - if (tag != 0x49492A00 && tag != 0x4D4D002A) { - Log.e(TAG, "Invalid byte order"); - return 0; - } - boolean littleEndian = (tag == 0x49492A00); - - // Get the offset and check if it is reasonable. - int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; - if (count < 10 || count > length) { - Log.e(TAG, "Invalid offset"); - return 0; - } - offset += count; - length -= count; - - // Get the count and go through all the elements. - count = pack(jpeg, offset - 2, 2, littleEndian); - while (count-- > 0 && length >= 12) { - // Get the tag and check if it is orientation. - tag = pack(jpeg, offset, 2, littleEndian); - if (tag == 0x0112) { - // We do not really care about type and count, do we? - int orientation = pack(jpeg, offset + 8, 2, littleEndian); - switch (orientation) { - case 1: - return 0; - case 3: - return 180; - case 6: - return 90; - case 8: - return 270; - } - Log.i(TAG, "Unsupported orientation"); - return 0; - } - offset += 12; - length -= 12; - } - } - - Log.i(TAG, "Orientation not found"); - return 0; - } - - private static int pack(byte[] bytes, int offset, int length, - boolean littleEndian) { - int step = 1; - if (littleEndian) { - offset += length - 1; - step = -1; - } - - int value = 0; - while (length-- > 0) { - value = (value << 8) | (bytes[offset] & 0xFF); - offset += step; - } - return value; - } - - private static boolean read(InputStream is, byte[] buf, int length) { - try { - return is.read(buf, 0, length) == length; - } catch (IOException ex) { - return false; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java deleted file mode 100644 index f7dda936..00000000 --- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java +++ /dev/null @@ -1,71 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.content.Intent; -import android.net.Uri; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; - -public class GeoHelper { - private static Pattern GEO_URI = Pattern.compile("geo:([\\-0-9.]+),([\\-0-9.]+)(?:,([\\-0-9.]+))?(?:\\?(.*))?", Pattern.CASE_INSENSITIVE); - - public static boolean isGeoUri(String body) { - return body != null && GEO_URI.matcher(body).matches(); - } - - public static ArrayList createGeoIntentsFromMessage(Message message) { - final ArrayList intents = new ArrayList(); - Matcher matcher = GEO_URI.matcher(message.getBody()); - if (!matcher.matches()) { - return intents; - } - double latitude; - double longitude; - try { - latitude = Double.parseDouble(matcher.group(1)); - if (latitude > 90.0 || latitude < -90.0) { - return intents; - } - longitude = Double.parseDouble(matcher.group(2)); - if (longitude > 180.0 || longitude < -180.0) { - return intents; - } - } catch (NumberFormatException nfe) { - return intents; - } - final Conversation conversation = message.getConversation(); - String label; - if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { - try { - label = "(" + URLEncoder.encode(message.getConversation().getName(), "UTF-8") + ")"; - } catch (UnsupportedEncodingException e) { - label = ""; - } - } else { - label = ""; - } - - Intent locationPluginIntent = new Intent("eu.siacs.conversations.location.show"); - locationPluginIntent.putExtra("latitude",latitude); - locationPluginIntent.putExtra("longitude",longitude); - if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { - locationPluginIntent.putExtra("name",conversation.getName()); - } - intents.add(locationPluginIntent); - - Intent geoIntent = new Intent(Intent.ACTION_VIEW); - geoIntent.setData(Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude) + "?q=" + String.valueOf(latitude) + "," + String.valueOf(longitude) + label)); - intents.add(geoIntent); - - Intent httpIntent = new Intent(Intent.ACTION_VIEW); - httpIntent.setData(Uri.parse("https://maps.google.com/maps?q=loc:"+String.valueOf(latitude) + "," + String.valueOf(longitude) +label)); - intents.add(httpIntent); - return intents; - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java deleted file mode 100644 index 9a689768..00000000 --- a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.util.List; - -import android.os.Bundle; - -public interface OnPhoneContactsLoadedListener { - public void onPhoneContactsLoaded(List phoneContacts); -} diff --git a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java deleted file mode 100644 index 8fe67234..00000000 --- a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java +++ /dev/null @@ -1,327 +0,0 @@ -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/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java deleted file mode 100644 index 99e8ebb8..00000000 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ /dev/null @@ -1,108 +0,0 @@ -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.content.pm.PackageManager; -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 List phoneContacts, final OnPhoneContactsLoadedListener listener) { - 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); - } - cursor.close(); - } - }); - 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); - mProfileCursor.close(); - if (uri == null) { - return null; - } else { - return Uri.parse(uri); - } - } - } - - public static String getVersionName(Context context) { - final String packageName = context == null ? null : context.getPackageName(); - if (packageName != null) { - try { - return context.getPackageManager().getPackageInfo(packageName, 0).versionName; - } catch (final PackageManager.NameNotFoundException e) { - return "unknown"; - } - } else { - return "unknown"; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java deleted file mode 100644 index 0bea6e53..00000000 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ /dev/null @@ -1,369 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.util.ArrayList; -import java.net.URLConnection; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Pattern; -import java.util.regex.Matcher; - -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.xmpp.jid.Jid; - -import android.content.Context; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.Spannable.Factory; -import android.text.style.ImageSpan; -import android.text.Spannable; -import android.util.Pair; - -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) { - return sameDay(date,new Date(System.currentTimeMillis())); - } - - public static boolean sameDay(long timestamp1, long timestamp2) { - return sameDay(new Date(timestamp1),new Date(timestamp2)); - } - - private static boolean sameDay(Date a, Date b) { - Calendar cal1 = Calendar.getInstance(); - Calendar cal2 = Calendar.getInstance(); - cal1.setTime(a); - cal2.setTime(b); - 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 final Map ANDROID_EMOTICONS = new HashMap(); - - private static final Factory spannableFactory = Spannable.Factory - .getInstance(); - - static { - addPattern(ANDROID_EMOTICONS, ":)", R.drawable.emo_im_happy); - addPattern(ANDROID_EMOTICONS, ":-)", R.drawable.emo_im_happy); - addPattern(ANDROID_EMOTICONS, ":(", R.drawable.emo_im_sad); - addPattern(ANDROID_EMOTICONS, ":-(", R.drawable.emo_im_sad); - addPattern(ANDROID_EMOTICONS, ";)", R.drawable.emo_im_winking); - addPattern(ANDROID_EMOTICONS, ";-)", R.drawable.emo_im_winking); - addPattern(ANDROID_EMOTICONS, ":P", - R.drawable.emo_im_tongue_sticking_out); - addPattern(ANDROID_EMOTICONS, ":-P", - R.drawable.emo_im_tongue_sticking_out); - addPattern(ANDROID_EMOTICONS, "=-O", R.drawable.emo_im_surprised); - addPattern(ANDROID_EMOTICONS, ":*", R.drawable.emo_im_kissing); - addPattern(ANDROID_EMOTICONS, ":-*", R.drawable.emo_im_kissing); - addPattern(ANDROID_EMOTICONS, ":O", R.drawable.emo_im_wtf); - addPattern(ANDROID_EMOTICONS, ":-O", R.drawable.emo_im_wtf); - addPattern(ANDROID_EMOTICONS, "B)", R.drawable.emo_im_cool); - addPattern(ANDROID_EMOTICONS, "B-)", R.drawable.emo_im_cool); - addPattern(ANDROID_EMOTICONS, "8)", R.drawable.emo_im_cool); - addPattern(ANDROID_EMOTICONS, "8-)", R.drawable.emo_im_cool); - addPattern(ANDROID_EMOTICONS, ":$", R.drawable.emo_im_money_mouth); - addPattern(ANDROID_EMOTICONS, ":-$", R.drawable.emo_im_money_mouth); - addPattern(ANDROID_EMOTICONS, ":-!", R.drawable.emo_im_foot_in_mouth); - addPattern(ANDROID_EMOTICONS, ":-[", R.drawable.emo_im_embarrassed); - addPattern(ANDROID_EMOTICONS, "O:)", R.drawable.emo_im_angel); - addPattern(ANDROID_EMOTICONS, "O:-)", R.drawable.emo_im_angel); - addPattern(ANDROID_EMOTICONS, ":\\", R.drawable.emo_im_undecided); - addPattern(ANDROID_EMOTICONS, ":-\\", R.drawable.emo_im_undecided); - addPattern(ANDROID_EMOTICONS, ":'(", R.drawable.emo_im_crying); - addPattern(ANDROID_EMOTICONS, ":D", R.drawable.emo_im_laughing); - addPattern(ANDROID_EMOTICONS, ":-D", R.drawable.emo_im_laughing); - addPattern(ANDROID_EMOTICONS, "O_o", R.drawable.emo_im_wtf); - addPattern(ANDROID_EMOTICONS, "o_O", R.drawable.emo_im_wtf); - addPattern(ANDROID_EMOTICONS, ">:O", R.drawable.emo_im_yelling); - addPattern(ANDROID_EMOTICONS, ">:0", R.drawable.emo_im_yelling); - addPattern(ANDROID_EMOTICONS, ":S", R.drawable.emo_im_lips_are_sealed); - addPattern(ANDROID_EMOTICONS, ":-S", R.drawable.emo_im_lips_are_sealed); - addPattern(ANDROID_EMOTICONS, "<3", R.drawable.emo_im_heart); - } - - private static void addPattern(Map map, String smile, - int resource) { - map.put(Pattern.compile(Pattern.quote(smile)), resource); - } - - private static boolean getSmiledText(Context context, Spannable spannable) { - boolean hasChanges = false; - Map emoticons = ANDROID_EMOTICONS; - for (Entry entry : emoticons.entrySet()) { - Matcher matcher = entry.getKey().matcher(spannable); - while (matcher.find()) { - boolean set = true; - for (ImageSpan span : spannable.getSpans(matcher.start(), - matcher.end(), ImageSpan.class)) - if (spannable.getSpanStart(span) >= matcher.start() - && spannable.getSpanEnd(span) <= matcher.end()) - spannable.removeSpan(span); - else { - set = false; - break; - } - if (set) { - spannable.setSpan(new ImageSpan(context, entry.getValue()), - matcher.start(), matcher.end(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - hasChanges = true; - } - } - } - return hasChanges; - } - - 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 transformAsciiEmoticonsToUtf8(String body) { - if (body != null) { - for (EmoticonPattern p : patterns) { - body = p.replaceAll(body); - } - } - return body; - } - - public static Spannable transformAsciiEmoticons(Context context, String body) { - Spannable spannable; - if (Config.UTF8_EMOTICONS) { - spannable = spannableFactory.newSpannable(transformAsciiEmoticonsToUtf8(body)); - } - else { - spannable = spannableFactory.newSpannable(body); - getSmiledText(context, spannable); - } - return spannable; - } - - public static int getColorForName(String name) { - if (name.isEmpty()) { - return 0xFF202020; - } - int colors[] = {0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5, - 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722, - 0xFF795548, 0xFF607d8b}; - return colors[(int) ((name.hashCode() & 0xffffffffl) % colors.length)]; - } - - public static Pair getMessagePreview(final Context context, final Message message) { - final Downloadable d = message.getDownloadable(); - if (d != null ) { - switch (d.getStatus()) { - case Downloadable.STATUS_CHECKING: - return new Pair<>(context.getString(R.string.checking_image),true); - case Downloadable.STATUS_DOWNLOADING: - return new Pair<>(context.getString(R.string.receiving_x_file, - getFileDescriptionString(context,message), - d.getProgress()),true); - case Downloadable.STATUS_OFFER: - case Downloadable.STATUS_OFFER_CHECK_FILESIZE: - return new Pair<>(context.getString(R.string.x_file_offered_for_download, - getFileDescriptionString(context,message)),true); - case Downloadable.STATUS_DELETED: - return new Pair<>(context.getString(R.string.file_deleted),true); - case Downloadable.STATUS_FAILED: - return new Pair<>(context.getString(R.string.file_transmission_failed),true); - case Downloadable.STATUS_UPLOADING: - if (message.getStatus() == Message.STATUS_OFFERED) { - return new Pair<>(context.getString(R.string.offering_x_file, - getFileDescriptionString(context, message)), true); - } else { - return new Pair<>(context.getString(R.string.sending_x_file, - getFileDescriptionString(context, message)), true); - } - default: - return new Pair<>("",false); - } - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - return new Pair<>(context.getString(R.string.encrypted_message_received),true); - } else if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - return new Pair<>(context.getString(R.string.received_x_file, - getFileDescriptionString(context, message)), true); - } else { - return new Pair<>(getFileDescriptionString(context,message),true); - } - } else { - if (message.getBody().startsWith(Message.ME_COMMAND)) { - return new Pair<>(message.getBody().replaceAll("^" + Message.ME_COMMAND, - UIHelper.getMessageDisplayName(message) + " "), false); - } else if (GeoHelper.isGeoUri(message.getBody())) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - return new Pair<>(context.getString(R.string.received_location),true); - } else { - return new Pair<>(context.getString(R.string.location), true); - } - } else{ - return new Pair<>(message.getBody(), false); - } - } - } - - public static String getFileDescriptionString(final Context context, final Message message) { - if (message.getType() == Message.TYPE_IMAGE) { - return context.getString(R.string.image); - } - final String path = message.getRelativeFilePath(); - if (path == null) { - return ""; - } - final String mime; - try { - mime = URLConnection.guessContentTypeFromName(path.replace("#","")); - } catch (final StringIndexOutOfBoundsException ignored) { - return context.getString(R.string.file); - } - if (mime == null) { - return context.getString(R.string.file); - } else if (mime.startsWith("audio/")) { - return context.getString(R.string.audio); - } else if(mime.startsWith("video/")) { - return context.getString(R.string.video); - } else if (mime.startsWith("image/")) { - return context.getString(R.string.image); - } else if (mime.contains("pdf")) { - return context.getString(R.string.pdf_document) ; - } else if (mime.contains("application/vnd.android.package-archive")) { - return context.getString(R.string.apk) ; - } else if (mime.contains("vcard")) { - return context.getString(R.string.vcard) ; - } else { - return mime; - } - } - - public static String getMessageDisplayName(final Message message) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - return getDisplayedMucCounterpart(message.getCounterpart()); - } else { - final Contact contact = message.getContact(); - return contact != null ? contact.getDisplayName() : ""; - } - } else { - if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - return getDisplayedMucCounterpart(message.getConversation().getJid()); - } else { - final Jid jid = message.getConversation().getAccount().getJid(); - return jid.hasLocalpart() ? jid.getLocalpart() : jid.toDomainJid().toString(); - } - } - } - - private static String getDisplayedMucCounterpart(final Jid counterpart) { - if (counterpart==null) { - return ""; - } else if (!counterpart.isBareJid()) { - return counterpart.getResourcepart().trim(); - } else { - return counterpart.toString().trim(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java deleted file mode 100644 index 4dee07cf..00000000 --- a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java +++ /dev/null @@ -1,12 +0,0 @@ -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/src/main/java/eu/siacs/conversations/utils/Xmlns.java b/src/main/java/eu/siacs/conversations/utils/Xmlns.java deleted file mode 100644 index 17fd2d26..00000000 --- a/src/main/java/eu/siacs/conversations/utils/Xmlns.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.siacs.conversations.utils; - -public final class Xmlns { - public static final String BLOCKING = "urn:xmpp:blocking"; - public static final String ROSTER = "jabber:iq:roster"; - public static final String REGISTER = "jabber:iq:register"; - public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; -} diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java deleted file mode 100644 index 92c0241e..00000000 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ /dev/null @@ -1,85 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.net.Uri; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; - -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class XmppUri { - - protected String jid; - protected boolean muc; - protected String fingerprint; - - public XmppUri(String uri) { - try { - parse(Uri.parse(uri)); - } catch (IllegalArgumentException e) { - try { - jid = Jid.fromString(uri).toBareJid().toString(); - } catch (InvalidJidException e2) { - jid = null; - } - } - } - - public XmppUri(Uri uri) { - parse(uri); - } - - protected void parse(Uri uri) { - String scheme = uri.getScheme(); - if ("xmpp".equalsIgnoreCase(scheme)) { - // sample: xmpp:jid@foo.com - muc = "join".equalsIgnoreCase(uri.getQuery()); - if (uri.getAuthority() != null) { - jid = uri.getAuthority(); - } else { - jid = uri.getSchemeSpecificPart().split("\\?")[0]; - } - fingerprint = parseFingerprint(uri.getQuery()); - } else if ("imto".equalsIgnoreCase(scheme)) { - // sample: imto://xmpp/jid@foo.com - try { - jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1]; - } catch (final UnsupportedEncodingException ignored) { - jid = null; - } - } else { - try { - jid = Jid.fromString(uri.toString()).toBareJid().toString(); - } catch (final InvalidJidException ignored) { - jid = null; - } - } - } - - protected String parseFingerprint(String query) { - if (query == null) { - return null; - } else { - final String NEEDLE = "otr-fingerprint="; - int index = query.indexOf(NEEDLE); - if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) { - return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40); - } else { - return null; - } - } - } - - public Jid getJid() { - try { - return this.jid == null ? null :Jid.fromString(this.jid.toLowerCase()); - } catch (InvalidJidException e) { - return null; - } - } - - public String getFingerprint() { - return this.fingerprint; - } -} diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java deleted file mode 100644 index 51708759..00000000 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ /dev/null @@ -1,171 +0,0 @@ -package eu.siacs.conversations.xml; - -import android.util.Log; - -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.XmlHelper; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - -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(final String name) { - return findChild(name) != null; - } - - public boolean hasChild(final String name, final 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 Jid getAttributeAsJid(String name) { - final String jid = this.getAttribute(name); - if (jid != null && !jid.isEmpty()) { - try { - return Jid.fromString(jid); - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "could not parse jid " + jid); - return null; - } - } - 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)); - } - - public boolean getAttributeAsBoolean(String name) { - String attr = getAttribute(name); - return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); - } -} diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java deleted file mode 100644 index b9ef979f..00000000 --- a/src/main/java/eu/siacs/conversations/xml/Tag.java +++ /dev/null @@ -1,104 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java deleted file mode 100644 index f11c1846..00000000 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ /dev/null @@ -1,114 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java deleted file mode 100644 index 52d3d46a..00000000 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ /dev/null @@ -1,141 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java deleted file mode 100644 index e45eba73..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import eu.siacs.conversations.entities.Account; - -public interface OnAdvancedStreamFeaturesLoaded { - public void onAdvancedStreamFeaturesAvailable(final Account account); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java deleted file mode 100644 index f09cf33d..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import eu.siacs.conversations.entities.Account; - -public interface OnBindListener { - public void onBind(Account account); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java deleted file mode 100644 index 20b17f02..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import eu.siacs.conversations.entities.Contact; - -public interface OnContactStatusChanged { - public void onContactStatusChanged(final Contact contact, final boolean online); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java deleted file mode 100644 index a4cff986..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java +++ /dev/null @@ -1,8 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java deleted file mode 100644 index 5f670d93..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import eu.siacs.conversations.entities.Account; - -public interface OnMessageAcknowledged { - public void onMessageAcknowledged(Account account, String id); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java deleted file mode 100644 index 325e945f..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java +++ /dev/null @@ -1,8 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java deleted file mode 100644 index 95c1acfc..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java +++ /dev/null @@ -1,8 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java deleted file mode 100644 index ad1d98cb..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import eu.siacs.conversations.entities.Account; - -public interface OnStatusChanged { - public void onStatusChanged(Account account); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java b/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java deleted file mode 100644 index 92e72cfa..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java +++ /dev/null @@ -1,13 +0,0 @@ -package eu.siacs.conversations.xmpp; - -public interface OnUpdateBlocklist { - // Use an enum instead of a boolean to make sure we don't run into the boolean trap - // (`onUpdateBlocklist(true)' doesn't read well, and could be confusing). - public static enum Status { - BLOCKED, - UNBLOCKED - } - - @SuppressWarnings("MethodNameSameAsClassName") - public void OnUpdateBlocklist(final Status status); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java deleted file mode 100644 index d4502d73..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.xmpp; - -public abstract interface PacketReceived { - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java deleted file mode 100644 index 48dc2150..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ /dev/null @@ -1,1125 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Parcelable; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.util.Log; -import android.util.Pair; -import android.util.SparseArray; - -import org.apache.http.conn.ssl.StrictHostnameVerifier; -import org.json.JSONException; -import org.json.JSONObject; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.math.BigInteger; -import java.net.ConnectException; -import java.net.IDN; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -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 eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.sasl.DigestMd5; -import eu.siacs.conversations.crypto.sasl.Plain; -import eu.siacs.conversations.crypto.sasl.SaslMechanism; -import eu.siacs.conversations.crypto.sasl.ScramSha1; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.generator.IqGenerator; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.DNSHelper; -import eu.siacs.conversations.utils.Xmlns; -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.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; -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 { - - private static final int PACKET_IQ = 0; - private static final int PACKET_MESSAGE = 1; - private static final int PACKET_PRESENCE = 2; - private final Context applicationContext; - protected Account account; - private final WakeLock wakeLock; - private Socket socket; - private XmlReader tagReader; - private TagWriter tagWriter; - private final Features features = new Features(this); - private boolean shouldBind = true; - private boolean shouldAuthenticate = true; - private Element streamFeatures; - private final HashMap> disco = new HashMap<>(); - - private String streamId = null; - private int smVersion = 3; - private final SparseArray messageReceipts = new SparseArray<>(); - - private int stanzasReceived = 0; - private int stanzasSent = 0; - private long lastPacketReceived = 0; - private long lastPingSent = 0; - private long lastConnect = 0; - private long lastSessionStarted = 0; - private int attempt = 0; - private final Map> 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 final ArrayList advancedStreamFeaturesLoadedListeners = new ArrayList<>(); - private OnMessageAcknowledged acknowledgedListener = null; - private XmppConnectionService mXmppConnectionService = null; - - private SaslMechanism saslMechanism; - - public XmppConnection(final Account account, final XmppConnectionService service) { - this.account = account; - this.wakeLock = service.getPowerManager().newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); - tagWriter = new TagWriter(); - mXmppConnectionService = service; - applicationContext = service.getApplicationContext(); - } - - protected void changeStatus(final Account.State nextStatus) { - if (account.getStatus() != nextStatus) { - if ((nextStatus == Account.State.OFFLINE) - && (account.getStatus() != Account.State.CONNECTING) - && (account.getStatus() != Account.State.ONLINE) - && (account.getStatus() != Account.State.DISABLED)) { - return; - } - if (nextStatus == Account.State.ONLINE) { - this.attempt = 0; - } - account.setStatus(nextStatus); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } - } - - protected void connect() { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); - features.encryptionEnabled = 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.State.CONNECTING); - final Bundle result = DNSHelper.getSRVRecord(account.getServer()); - final ArrayList values = result.getParcelableArrayList("values"); - if ("timeout".equals(result.getString("error"))) { - throw new IOException("timeout in dns"); - } else if (values != null) { - int i = 0; - boolean socketError = true; - while (socketError && values.size() > i) { - final Bundle namePort = (Bundle) values.get(i); - try { - String srvRecordServer; - try { - srvRecordServer=IDN.toASCII(namePort.getString("name")); - } catch (final IllegalArgumentException e) { - // TODO: Handle me?` - srvRecordServer = ""; - } - final int srvRecordPort = namePort.getInt("port"); - final String srvIpServer = namePort.getString("ip"); - final InetSocketAddress addr; - if (srvIpServer != null) { - addr = new InetSocketAddress(srvIpServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " + srvRecordServer - + "[" + srvIpServer + "]:" + srvRecordPort); - } else { - addr = new InetSocketAddress(srvRecordServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort); - } - socket = new Socket(); - socket.connect(addr, 20000); - socketError = false; - } catch (final UnknownHostException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); - i++; - } catch (final IOException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); - i++; - } - } - if (socketError) { - throw new UnknownHostException(); - } - } else if (result.containsKey("error") - && "nosrv".equals(result.getString("error", null))) { - socket = new Socket(account.getServer().getDomainpart(), 5222); - } else { - throw new IOException("timeout in dns"); - } - final OutputStream out = socket.getOutputStream(); - tagWriter.setOutputStream(out); - final 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 { - throw new IOException("unknown tag on connect"); - } - } - if (socket.isConnected()) { - socket.close(); - } - } catch (final UnknownHostException | ConnectException e) { - this.changeStatus(Account.State.SERVER_NOT_FOUND); - } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); - this.changeStatus(Account.State.OFFLINE); - this.attempt--; //don't count attempt when reconnecting instantly anyway - } finally { - if (wakeLock.isHeld()) { - try { - wakeLock.release(); - } catch (final RuntimeException ignored) { - } - } - } - } - - @Override - public void run() { - try { - if (socket != null) { - socket.close(); - } - } catch (final IOException ignored) { - - } - connect(); - } - - private void processStream(final 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("success")) { - final String challenge = tagReader.readElement(nextTag).getContent(); - try { - saslMechanism.getResponse(challenge); - } catch (final SaslMechanism.AuthenticationException e) { - disconnect(true); - Log.e(Config.LOGTAG, String.valueOf(e)); - } - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in"); - account.setKey(Account.PINNED_MECHANISM_KEY, - String.valueOf(saslMechanism.getPriority())); - tagReader.reset(); - sendStartStream(); - processStream(tagReader.readTag()); - break; - } else if (nextTag.isStart("failure")) { - tagReader.readElement(nextTag); - changeStatus(Account.State.UNAUTHORIZED); - } else if (nextTag.isStart("challenge")) { - final String challenge = tagReader.readElement(nextTag).getContent(); - final Element response = new Element("response"); - response.setAttribute("xmlns", - "urn:ietf:params:xml:ns:xmpp-sasl"); - try { - response.setContent(saslMechanism.getResponse(challenge)); - } catch (final SaslMechanism.AuthenticationException e) { - // TODO: Send auth abort tag. - Log.e(Config.LOGTAG, e.toString()); - } - tagWriter.writeElement(response); - } else if (nextTag.isStart("enabled")) { - final Element enabled = tagReader.readElement(nextTag); - if ("true".equals(enabled.getAttribute("resume"))) { - this.streamId = enabled.getAttribute("id"); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": stream managment(" + smVersion - + ") enabled (resumable)"); - } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": stream managment(" + smVersion + ") enabled"); - } - this.lastSessionStarted = SystemClock.elapsedRealtime(); - this.stanzasReceived = 0; - final RequestPacket r = new RequestPacket(smVersion); - tagWriter.writeStanzaAsync(r); - } else if (nextTag.isStart("resumed")) { - lastPacketReceived = SystemClock.elapsedRealtime(); - final Element resumed = tagReader.readElement(nextTag); - final String h = resumed.getAttribute("h"); - try { - final int serverCount = Integer.parseInt(h); - if (serverCount != stanzasSent) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": session resumed with lost packages"); - stanzasSent = serverCount; - } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": 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 (final NumberFormatException ignored) { - } - sendServiceDiscoveryInfo(account.getServer()); - sendServiceDiscoveryItems(account.getServer()); - sendInitialPing(); - } else if (nextTag.isStart("r")) { - tagReader.readElement(nextTag); - final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); - tagWriter.writeStanzaAsync(ack); - } else if (nextTag.isStart("a")) { - final Element ack = tagReader.readElement(nextTag); - lastPacketReceived = SystemClock.elapsedRealtime(); - final int serverSequence = Integer.parseInt(ack.getAttribute("h")); - final 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().toBareJid().toString() + ": resumption failed"); - streamId = null; - if (account.getStatus() != Account.State.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.State.ONLINE) { - account. setStatus(Account.State.OFFLINE); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } - } - - private void sendInitialPing() { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping"); - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setFrom(account.getJid()); - iq.addChild("ping", "urn:xmpp:ping"); - this.sendIqPacket(iq, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": online with resource " + account.getResource()); - changeStatus(Account.State.ONLINE); - } - }); - } - - private Element processPacket(final Tag currentTag, final 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()) { - final Element child = tagReader.readElement(nextTag); - final 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; - lastPacketReceived = SystemClock.elapsedRealtime(); - return element; - } - - private void processIq(final Tag currentTag) throws XmlPullParserException, IOException { - final 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())) { - final Pair packetCallbackDuple = packetCallbacks.get(packet.getId()); - // Packets to the server should have responses from the server - if (packetCallbackDuple.first.toServer(account)) { - if (packet.fromServer(account)) { - packetCallbackDuple.second.onIqPacketReceived(account, packet); - packetCallbacks.remove(packet.getId()); - } else { - Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet"); - } - } else { - if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) { - packetCallbackDuple.second.onIqPacketReceived(account, packet); - packetCallbacks.remove(packet.getId()); - } else { - Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet"); - } - } - } else if (packet.getType() == IqPacket.TYPE.GET|| packet.getType() == IqPacket.TYPE.SET) { - this.unregisteredIqListener.onIqPacketReceived(account, packet); - } - } - } - - private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException { - final MessagePacket packet = (MessagePacket) processPacket(currentTag,PACKET_MESSAGE); - this.messageListener.onMessagePacketReceived(account, packet); - } - - private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException { - PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); - this.presenceListener.onPresencePacketReceived(account, packet); - } - - private void sendStartTLS() throws IOException { - final 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(final Tag currentTag) throws XmlPullParserException, IOException { - tagReader.readTag(); - try { - final SSLContext sc = SSLContext.getInstance("TLS"); - sc.init(null,new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},mXmppConnectionService.getRNG()); - final SSLSocketFactory factory = sc.getSocketFactory(); - final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); - final InetAddress address = socket == null ? null : socket.getInetAddress(); - - if (factory == null || address == null || verifier == null) { - throw new IOException("could not setup ssl"); - } - - final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); - - if (sslSocket == null) { - throw new IOException("could not initialize ssl socket"); - } - - final String[] supportProtocols; - final Collection supportedProtocols = new LinkedList<>( - Arrays.asList(sslSocket.getSupportedProtocols())); - supportedProtocols.remove("SSLv3"); - supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]); - - sslSocket.setEnabledProtocols(supportProtocols); - - final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( - sslSocket.getSupportedCipherSuites()); - //Log.d(Config.LOGTAG, "Using ciphers: " + Arrays.toString(cipherSuites)); - if (cipherSuites.length > 0) { - sslSocket.setEnabledCipherSuites(cipherSuites); - } - - if (!verifier.verify(account.getServer().getDomainpart(),sslSocket.getSession())) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); - disconnect(true); - changeStatus(Account.State.SECURITY_ERROR); - } - tagReader.setInputStream(sslSocket.getInputStream()); - tagWriter.setOutputStream(sslSocket.getOutputStream()); - sendStartStream(); - Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established"); - features.encryptionEnabled = true; - processStream(tagReader.readTag()); - sslSocket.close(); - } catch (final NoSuchAlgorithmException | KeyManagementException e1) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); - disconnect(true); - changeStatus(Account.State.SECURITY_ERROR); - } - } - - private void processStreamFeatures(final Tag currentTag) - throws XmlPullParserException, IOException { - this.streamFeatures = tagReader.readElement(currentTag); - if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) { - sendStartTLS(); - } else if (this.streamFeatures.hasChild("register") - && account.isOptionSet(Account.OPTION_REGISTER) - && features.encryptionEnabled) { - sendRegistryRequest(); - } else if (!this.streamFeatures.hasChild("register") - && account.isOptionSet(Account.OPTION_REGISTER)) { - changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED); - disconnect(true); - } else if (this.streamFeatures.hasChild("mechanisms") - && shouldAuthenticate && features.encryptionEnabled) { - final List mechanisms = extractMechanisms(streamFeatures - .findChild("mechanisms")); - final Element auth = new Element("auth"); - auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); - if (mechanisms.contains("SCRAM-SHA-1")) { - saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains("PLAIN")) { - saslMechanism = new Plain(tagWriter, account); - } else if (mechanisms.contains("DIGEST-MD5")) { - saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); - } - final JSONObject keys = account.getKeys(); - try { - if (keys.has(Account.PINNED_MECHANISM_KEY) && - keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority() ) { - Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + - " has lower priority (" + String.valueOf(saslMechanism.getPriority()) + - ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) + - "). Possible downgrade attack?"); - disconnect(true); - changeStatus(Account.State.SECURITY_ERROR); - } - } catch (final JSONException e) { - Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); - } - Log.d(Config.LOGTAG,account.getJid().toString()+": Authenticating with " + saslMechanism.getMechanism()); - auth.setAttribute("mechanism", saslMechanism.getMechanism()); - if (!saslMechanism.getClientFirstMessage().isEmpty()) { - auth.setContent(saslMechanism.getClientFirstMessage()); - } - tagWriter.writeElement(auth); - } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" - + smVersion) - && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId, - stanzasReceived, smVersion); - this.tagWriter.writeStanzaAsync(resume); - } else if (this.streamFeatures.hasChild("bind") && shouldBind) { - sendBindRequest(); - } else { - disconnect(true); - changeStatus(Account.State.INCOMPATIBLE_SERVER); - } - } - - private List extractMechanisms(final Element stream) { - final ArrayList mechanisms = new ArrayList<>(stream - .getChildren().size()); - for (final Element child : stream.getChildren()) { - mechanisms.add(child.getContent()); - } - return mechanisms; - } - - private void sendRegistryRequest() { - final IqPacket register = new IqPacket(IqPacket.TYPE.GET); - register.query("jabber:iq:register"); - register.setTo(account.getServer()); - sendIqPacket(register, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final Element instructions = packet.query().findChild("instructions"); - if (packet.query().hasChild("username") - && (packet.query().hasChild("password"))) { - final IqPacket register = new IqPacket(IqPacket.TYPE.SET); - final Element username = new Element("username") - .setContent(account.getUsername()); - final 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(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setOption(Account.OPTION_REGISTER, - false); - changeStatus(Account.State.REGISTRATION_SUCCESSFUL); - } else if (packet.hasChild("error") - && (packet.findChild("error") - .hasChild("conflict"))) { - changeStatus(Account.State.REGISTRATION_CONFLICT); - } else { - changeStatus(Account.State.REGISTRATION_FAILED); - Log.d(Config.LOGTAG, packet.toString()); - } - disconnect(true); - } - }); - } else { - changeStatus(Account.State.REGISTRATION_FAILED); - disconnect(true); - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": could not register. instructions are" - + instructions.getContent()); - } - } - }); - } - - private void sendBindRequest() { - while(!mXmppConnectionService.areMessagesInitialized()) { - try { - Thread.sleep(500); - } catch (final InterruptedException ignored) { - } - } - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") - .addChild("resource").setContent(account.getResource()); - this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final Element bind = packet.findChild("bind"); - if (bind != null) { - final Element jid = bind.findChild("jid"); - if (jid != null && jid.getContent() != null) { - try { - account.setResource(Jid.fromString(jid.getContent()).getResourcepart()); - } catch (final InvalidJidException e) { - // TODO: Handle the case where an external JID is technically invalid? - } - if (streamFeatures.hasChild("session")) { - sendStartSession(); - } else { - sendPostBindInitialization(); - } - } else { - disconnect(true); - } - } else { - disconnect(true); - } - } - }); - } - - private void sendStartSession() { - final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); - startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); - this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - sendPostBindInitialization(); - } else { - disconnect(true); - } - } - }); - } - - private void sendPostBindInitialization() { - smVersion = 0; - if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { - smVersion = 3; - } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { - smVersion = 2; - } - if (smVersion != 0) { - final EnablePacket enable = new EnablePacket(smVersion); - tagWriter.writeStanzaAsync(enable); - stanzasSent = 0; - messageReceipts.clear(); - } - features.carbonsEnabled = false; - features.blockListRequested = false; - disco.clear(); - sendServiceDiscoveryInfo(account.getServer()); - sendServiceDiscoveryItems(account.getServer()); - if (bindListener != null) { - bindListener.onBind(account); - } - sendInitialPing(); - } - - private void sendServiceDiscoveryInfo(final Jid server) { - if (disco.containsKey(server.toDomainJid().toString())) { - if (account.getServer().equals(server.toDomainJid())) { - enableAdvancedStreamFeatures(); - } - } else { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setTo(server.toDomainJid()); - iq.query("http://jabber.org/protocol/disco#info"); - this.sendIqPacket(iq, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final List elements = packet.query().getChildren(); - final List features = new ArrayList<>(); - for (final Element element : elements) { - if (element.getName().equals("identity")) { - if ("irc".equals(element.getAttribute("type"))) { - //add fake feature to not confuse irc and real muc - features.add("siacs:no:muc"); - } - } else if (element.getName().equals("feature")) { - features.add(element.getAttribute("var")); - } - } - disco.put(server.toDomainJid().toString(), features); - - if (account.getServer().equals(server.toDomainJid())) { - enableAdvancedStreamFeatures(); - for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { - listener.onAdvancedStreamFeaturesAvailable(account); - } - } - } - }); - } - } - - private void enableAdvancedStreamFeatures() { - if (getFeatures().carbons() && !features.carbonsEnabled) { - sendEnableCarbons(); - } - if (getFeatures().blocking() && !features.blockListRequested) { - Log.d(Config.LOGTAG, "Requesting block list"); - this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); - } - } - - private void sendServiceDiscoveryItems(final Jid server) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setTo(server.toDomainJid()); - iq.query("http://jabber.org/protocol/disco#items"); - this.sendIqPacket(iq, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final List elements = packet.query().getChildren(); - for (final Element element : elements) { - if (element.getName().equals("item")) { - final Jid jid = element.getAttributeAsJid("jid"); - if (jid != null && !jid.equals(account.getServer())) { - sendServiceDiscoveryInfo(jid); - } - } - } - } - }); - } - - private void sendEnableCarbons() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.addChild("enable", "urn:xmpp:carbons:2"); - this.sendIqPacket(iq, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (!packet.hasChild("error")) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": successfully enabled carbons"); - features.carbonsEnabled = true; - } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": error enableing carbons " + packet.toString()); - } - } - }); - } - - private void processStreamError(final Tag currentTag) - throws XmlPullParserException, IOException { - final Element streamError = tagReader.readElement(currentTag); - if (streamError != null && streamError.hasChild("conflict")) { - final String resource = account.getResource().split("\\.")[0]; - account.setResource(resource + "." + nextRandomId()); - Log.d(Config.LOGTAG, - account.getJid().toBareJid() + ": switching resource due to conflict (" - + account.getResource() + ")"); - } - } - - private void sendStartStream() throws IOException { - final Tag stream = Tag.start("stream:stream"); - stream.setAttribute("from", account.getJid().toBareJid().toString()); - stream.setAttribute("to", account.getServer().toString()); - 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, mXmppConnectionService.getRNG()).toString(32); - } - - public void sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { - packet.setFrom(account.getJid()); - this.sendUnmodifiedIqPacket(packet,callback); - - } - - private synchronized void sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { - if (packet.getId() == null) { - final String id = nextRandomId(); - packet.setAttribute("id", id); - } - if (callback != null) { - if (packet.getId() == null) { - packet.setId(nextRandomId()); - } - packetCallbacks.put(packet.getId(), new Pair<>(packet, callback)); - } - this.sendPacket(packet); - } - - public void sendMessagePacket(final MessagePacket packet) { - this.sendPacket(packet); - } - - public void sendPresencePacket(final PresencePacket packet) { - this.sendPacket(packet); - } - - private synchronized void sendPacket(final AbstractStanza packet) { - final String name = packet.getName(); - if (name.equals("iq") || name.equals("message") || name.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)); - } - } - - public void sendPing() { - if (streamFeatures.hasChild("sm")) { - tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); - } else { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setFrom(account.getJid()); - iq.addChild("ping", "urn:xmpp:ping"); - this.sendIqPacket(iq, null); - } - this.lastPingSent = SystemClock.elapsedRealtime(); - } - - public void setOnMessagePacketReceivedListener( - final OnMessagePacketReceived listener) { - this.messageListener = listener; - } - - public void setOnUnregisteredIqPacketReceivedListener( - final OnIqPacketReceived listener) { - this.unregisteredIqListener = listener; - } - - public void setOnPresencePacketReceivedListener( - final OnPresencePacketReceived listener) { - this.presenceListener = listener; - } - - public void setOnJinglePacketReceivedListener( - final OnJinglePacketReceived listener) { - this.jingleListener = listener; - } - - public void setOnStatusChangedListener(final OnStatusChanged listener) { - this.statusListener = listener; - } - - public void setOnBindListener(final OnBindListener listener) { - this.bindListener = listener; - } - - public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) { - this.acknowledgedListener = listener; - } - - public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) { - if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) { - this.advancedStreamFeaturesLoadedListeners.add(listener); - } - } - - public void disconnect(final boolean force) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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 (final IOException e) { - Log.d(Config.LOGTAG, - "io exception during disconnect"); - } catch (final InterruptedException e) { - Log.d(Config.LOGTAG, "interrupted"); - } - } - } - }).start(); - } catch (final IOException e) { - Log.d(Config.LOGTAG, "io exception during disconnect"); - } - } - - public List findDiscoItemsByFeature(final String feature) { - final List items = new ArrayList<>(); - for (final Entry> cursor : disco.entrySet()) { - if (cursor.getValue().contains(feature)) { - items.add(cursor.getKey()); - } - } - return items; - } - - public String findDiscoItemByFeature(final String feature) { - final 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() { - for (final Entry> cursor : disco.entrySet()) { - final List value = cursor.getValue(); - if (value.contains("http://jabber.org/protocol/muc") && !value.contains("jabber:iq:gateway") && !value.contains("siacs:no:muc")) { - return cursor.getKey(); - } - } - return null; - } - - public int getTimeToNextAttempt() { - final int interval = (int) (25 * Math.pow(1.5, attempt)); - final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); - return interval - secondsSinceLast; - } - - public int getAttempt() { - return this.attempt; - } - - public Features getFeatures() { - return this.features; - } - - public long getLastSessionEstablished() { - final 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.lastPacketReceived; - } - - public void sendActive() { - this.sendPacket(new ActivePacket()); - } - - public void sendInactive() { - this.sendPacket(new InactivePacket()); - } - - public void resetAttemptCount() { - this.attempt = 0; - this.lastConnect = 0; - } - - public class Features { - XmppConnection connection; - private boolean carbonsEnabled = false; - private boolean encryptionEnabled = false; - private boolean blockListRequested = false; - - public Features(final XmppConnection connection) { - this.connection = connection; - } - - private boolean hasDiscoFeature(final Jid server, final String feature) { - return connection.disco.containsKey(server.toDomainJid().toString()) && - connection.disco.get(server.toDomainJid().toString()).contains(feature); - } - - public boolean carbons() { - return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); - } - - public boolean blocking() { - return hasDiscoFeature(account.getServer(), Xmlns.BLOCKING); - } - - public boolean register() { - return hasDiscoFeature(account.getServer(), Xmlns.REGISTER); - } - - public boolean sm() { - return streamId != null; - } - - public boolean csi() { - return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0"); - } - - public boolean pubsub() { - return hasDiscoFeature(account.getServer(), - "http://jabber.org/protocol/pubsub#publish"); - } - - public boolean mam() { - return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); - } - - public boolean advancedStreamFeaturesLoaded() { - return disco.containsKey(account.getServer().toString()); - } - - public boolean rosterVersioning() { - return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver"); - } - - public void setBlockListRequested(boolean value) { - this.blockListRequested = value; - } - } - - private IqGenerator getIqGenerator() { - return mXmppConnectionService.getIqGenerator(); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java deleted file mode 100644 index f85efbdb..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java +++ /dev/null @@ -1,32 +0,0 @@ -package eu.siacs.conversations.xmpp.chatstate; - -import eu.siacs.conversations.xml.Element; - -public enum ChatState { - - ACTIVE, INACTIVE, GONE, COMPOSING, PAUSED, mIncomingChatState; - - public static ChatState parse(Element element) { - final String NAMESPACE = "http://jabber.org/protocol/chatstates"; - if (element.hasChild("active",NAMESPACE)) { - return ACTIVE; - } else if (element.hasChild("inactive",NAMESPACE)) { - return INACTIVE; - } else if (element.hasChild("composing",NAMESPACE)) { - return COMPOSING; - } else if (element.hasChild("gone",NAMESPACE)) { - return GONE; - } else if (element.hasChild("paused",NAMESPACE)) { - return PAUSED; - } else { - return null; - } - } - - public static Element toElement(ChatState state) { - final String NAMESPACE = "http://jabber.org/protocol/chatstates"; - final Element element = new Element(state.toString().toLowerCase()); - element.setAttribute("xmlns",NAMESPACE); - return element; - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java deleted file mode 100644 index 44794c80..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ /dev/null @@ -1,85 +0,0 @@ -package eu.siacs.conversations.xmpp.forms; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import eu.siacs.conversations.xml.Element; - -public class Data extends Element { - - public Data() { - super("x"); - this.setAttribute("xmlns","jabber:x:data"); - } - - public List getFields() { - ArrayList fields = new ArrayList(); - for(Element child : getChildren()) { - if (child.getName().equals("field")) { - fields.add(Field.parse(child)); - } - } - return fields; - } - - public Field getFieldByName(String needle) { - for(Element child : getChildren()) { - if (child.getName().equals("field") && needle.equals(child.getAttribute("var"))) { - return Field.parse(child); - } - } - return null; - } - - public void put(String name, String value) { - Field field = getFieldByName(name); - if (field == null) { - field = new Field(name); - this.addChild(field); - } - field.setValue(value); - } - - public void put(String name, Collection values) { - Field field = getFieldByName(name); - if (field == null) { - field = new Field(name); - this.addChild(field); - } - field.setValues(values); - } - - public void submit() { - this.setAttribute("type","submit"); - removeNonFieldChildren(); - for(Field field : getFields()) { - field.removeNonValueChildren(); - } - } - - private void removeNonFieldChildren() { - for(Iterator iterator = this.children.iterator(); iterator.hasNext();) { - Element element = iterator.next(); - if (!element.getName().equals("field")) { - iterator.remove(); - } - } - } - - public static Data parse(Element element) { - Data data = new Data(); - data.setAttributes(element.getAttributes()); - data.setChildren(element.getChildren()); - return data; - } - - public void setFormType(String formType) { - this.put("FORM_TYPE",formType); - } - - public String getFormType() { - return this.getAttribute("FORM_TYPE"); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java deleted file mode 100644 index ee2c51a9..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java +++ /dev/null @@ -1,50 +0,0 @@ -package eu.siacs.conversations.xmpp.forms; - -import java.util.Collection; -import java.util.Iterator; - -import eu.siacs.conversations.xml.Element; - -public class Field extends Element { - - public Field(String name) { - super("field"); - this.setAttribute("var",name); - } - - private Field() { - super("field"); - } - - public String getName() { - return this.getAttribute("var"); - } - - public void setValue(String value) { - this.children.clear(); - this.addChild("value").setContent(value); - } - - public void setValues(Collection values) { - this.children.clear(); - for(String value : values) { - this.addChild("value").setContent(value); - } - } - - public void removeNonValueChildren() { - for(Iterator iterator = this.children.iterator(); iterator.hasNext();) { - Element element = iterator.next(); - if (!element.getName().equals("value")) { - iterator.remove(); - } - } - } - - public static Field parse(Element element) { - Field field = new Field(); - field.setAttributes(element.getAttributes()); - field.setChildren(element.getChildren()); - return field; - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java deleted file mode 100644 index 164e8849..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java +++ /dev/null @@ -1,49 +0,0 @@ -package eu.siacs.conversations.xmpp.jid; - -public class InvalidJidException extends Exception { - - // This is probably not the "Java way", but the "Java way" means we'd have a ton of extra tiny, - // annoying classes floating around. I like this. - public final static String INVALID_LENGTH = "JID must be between 0 and 3071 characters"; - public final static String INVALID_PART_LENGTH = "JID part must be between 0 and 1023 characters"; - public final static String INVALID_CHARACTER = "JID contains an invalid character"; - public final static String STRINGPREP_FAIL = "The STRINGPREP operation has failed for the given JID"; - public final static String IS_NULL = "JID can not be NULL"; - - /** - * Constructs a new {@code Exception} that includes the current stack trace. - */ - public InvalidJidException() { - } - - /** - * Constructs a new {@code Exception} with the current stack trace and the - * specified detail message. - * - * @param detailMessage the detail message for this exception. - */ - public InvalidJidException(final String detailMessage) { - super(detailMessage); - } - - /** - * Constructs a new {@code Exception} with the current stack trace, the - * specified detail message and the specified cause. - * - * @param detailMessage the detail message for this exception. - * @param throwable the cause of this exception. - */ - public InvalidJidException(final String detailMessage, final Throwable throwable) { - super(detailMessage, throwable); - } - - /** - * Constructs a new {@code Exception} with the current stack trace and the - * specified cause. - * - * @param throwable the cause of this exception. - */ - public InvalidJidException(final Throwable throwable) { - super(throwable); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java deleted file mode 100644 index 295e067a..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java +++ /dev/null @@ -1,219 +0,0 @@ -package eu.siacs.conversations.xmpp.jid; - -import android.util.LruCache; - -import net.java.otr4j.session.SessionID; - -import java.net.IDN; - -import eu.siacs.conversations.Config; -import gnu.inet.encoding.Stringprep; -import gnu.inet.encoding.StringprepException; - -/** - * The `Jid' class provides an immutable representation of a JID. - */ -public final class Jid { - - private static LruCache cache = new LruCache<>(1024); - - private final String localpart; - private final String domainpart; - private final String resourcepart; - - // It's much more efficient to store the ful JID as well as the parts instead of figuring them - // all out every time (since some characters are displayed but aren't used for comparisons). - private final String displayjid; - - public String getLocalpart() { - return localpart; - } - - public String getDomainpart() { - return IDN.toUnicode(domainpart); - } - - public String getResourcepart() { - return resourcepart; - } - - public static Jid fromSessionID(final SessionID id) throws InvalidJidException{ - if (id.getUserID().isEmpty()) { - return Jid.fromString(id.getAccountID()); - } else { - return Jid.fromString(id.getAccountID()+"/"+id.getUserID()); - } - } - - public static Jid fromString(final String jid) throws InvalidJidException { - return Jid.fromString(jid, false); - } - - public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException { - return new Jid(jid, safe); - } - - public static Jid fromParts(final String localpart, - final String domainpart, - final String resourcepart) throws InvalidJidException { - String out; - if (localpart == null || localpart.isEmpty()) { - out = domainpart; - } else { - out = localpart + "@" + domainpart; - } - if (resourcepart != null && !resourcepart.isEmpty()) { - out = out + "/" + resourcepart; - } - return new Jid(out, false); - } - - private Jid(final String jid, final boolean safe) throws InvalidJidException { - if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL); - - Jid fromCache = Jid.cache.get(jid); - if (fromCache != null) { - displayjid = fromCache.displayjid; - localpart = fromCache.localpart; - domainpart = fromCache.domainpart; - resourcepart = fromCache.resourcepart; - return; - } - - // Hackish Android way to count the number of chars in a string... should work everywhere. - final int atCount = jid.length() - jid.replace("@", "").length(); - final int slashCount = jid.length() - jid.replace("/", "").length(); - - // Throw an error if there's anything obvious wrong with the JID... - if (jid.isEmpty() || jid.length() > 3071) { - throw new InvalidJidException(InvalidJidException.INVALID_LENGTH); - } - - // Go ahead and check if the localpart or resourcepart is empty. - if (jid.startsWith("@") || (jid.endsWith("@") && slashCount == 0) || jid.startsWith("/") || (jid.endsWith("/") && slashCount < 2)) { - throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER); - } - - String finaljid; - - final int domainpartStart; - final int atLoc = jid.indexOf("@"); - final int slashLoc = jid.indexOf("/"); - // If there is no "@" in the JID (eg. "example.net" or "example.net/resource") - // or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"): - if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) { - localpart = ""; - finaljid = ""; - domainpartStart = 0; - } else { - final String lp = jid.substring(0, atLoc); - try { - localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp); - } catch (final StringprepException e) { - throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); - } - if (localpart.isEmpty() || localpart.length() > 1023) { - throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); - } - domainpartStart = atLoc + 1; - finaljid = lp + "@"; - } - - final String dp; - if (slashCount > 0) { - final String rp = jid.substring(slashLoc + 1, jid.length()); - try { - resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp); - } catch (final StringprepException e) { - throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); - } - if (resourcepart.isEmpty() || resourcepart.length() > 1023) { - throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); - } - dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES); - finaljid = finaljid + dp + "/" + rp; - } else { - resourcepart = ""; - dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()), - IDN.USE_STD3_ASCII_RULES); - finaljid = finaljid + dp; - } - - // Remove trailing "." before storing the domain part. - if (dp.endsWith(".")) { - try { - domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES); - } catch (final IllegalArgumentException e) { - throw new InvalidJidException(e); - } - } else { - try { - domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES); - } catch (final IllegalArgumentException e) { - throw new InvalidJidException(e); - } - } - - // TODO: Find a proper domain validation library; validate individual parts, separators, etc. - if (domainpart.isEmpty() || domainpart.length() > 1023) { - throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); - } - - Jid.cache.put(jid, this); - - this.displayjid = finaljid; - } - - public Jid toBareJid() { - try { - return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, ""); - } catch (final InvalidJidException e) { - // This should never happen. - return null; - } - } - - public Jid toDomainJid() { - try { - return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart()); - } catch (final InvalidJidException e) { - // This should never happen. - return null; - } - } - - @Override - public String toString() { - return displayjid; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final Jid jid = (Jid) o; - - return jid.hashCode() == this.hashCode(); - } - - @Override - public int hashCode() { - int result = localpart.hashCode(); - result = 31 * result + domainpart.hashCode(); - result = 31 * result + resourcepart.hashCode(); - return result; - } - - public boolean hasLocalpart() { - return !localpart.isEmpty(); - } - - public boolean isBareJid() { - return this.resourcepart.isEmpty(); - } - - public boolean isDomainJid() { - return !this.hasLocalpart(); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java deleted file mode 100644 index dcadb92f..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java +++ /dev/null @@ -1,147 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.Jid; - -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 Jid 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(final Jid jid) { - this.jid = jid; - } - - public Jid 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) { - switch (type) { - case "proxy": - this.type = TYPE_PROXY; - break; - case "direct": - this.type = TYPE_DIRECT; - break; - default: - this.type = TYPE_UNKNOWN; - break; - } - } - - 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 != null && 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.getAttributeAsJid("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().toString()); - 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/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java deleted file mode 100644 index 0db0deee..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ /dev/null @@ -1,971 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import java.net.URLConnection; -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.net.Uri; -import android.os.SystemClock; -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.DownloadablePlaceholder; -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.jid.Jid; -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 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_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 = Downloadable.STATUS_UNKNOWN; - private Message message; - private String sessionId; - private Account account; - private Jid initiator; - private Jid 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 int mProgress = 0; - private long mLastGuiRefresh = 0; - - 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) { - fail(); - } - } - }; - - final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { - - @Override - public void onFileTransmitted(DownloadableFile file) { - if (responder.equals(account.getJid())) { - sendSuccess(); - if (acceptedAutomatically) { - message.markUnread(); - JingleConnection.this.mXmppConnectionService - .getNotificationService().push(message); - } - mXmppConnectionService.getFileBackend().updateFileParams(message); - mXmppConnectionService.databaseBackend.createMessage(message); - mXmppConnectionService.markMessage(message, - Message.STATUS_RECEIVED); - } else { - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - file.delete(); - } - } - 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.fail(); - } - }; - - private OnProxyActivated onProxyActivated = new OnProxyActivated() { - - @Override - public void success() { - if (initiator.equals(account.getJid())) { - 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 Jid 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.fail(); - } else if (reason.hasChild("success")) { - this.receiveSuccess(); - } else { - this.fail(); - } - } else { - this.fail(); - } - } 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.generateResponse(IqPacket.TYPE.RESULT); - - } else { - response = packet.generateResponse(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.message.setDownloadable(this); - this.mStatus = Downloadable.STATUS_UPLOADING; - this.account = message.getConversation().getAccount(); - this.initiator = this.account.getJid(); - 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().toBareJid(), false); - this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); - this.message.setStatus(Message.STATUS_RECEIVED); - this.mStatus = Downloadable.STATUS_OFFER; - this.message.setDownloadable(this); - final Jid from = packet.getFrom(); - this.message.setCounterpart(from); - this.account = account; - this.initiator = packet.getFrom(); - this.responder = this.account.getJid(); - 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) { - String[] filename = fileNameElement.getContent() - .toLowerCase(Locale.US).toLowerCase().split("\\."); - String extension = filename[filename.length - 1]; - if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { - message.setType(Message.TYPE_IMAGE); - message.setRelativeFilePath(message.getUuid()+"."+extension); - } else if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains( - filename[filename.length - 1])) { - if (filename.length == 3) { - extension = filename[filename.length - 2]; - if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { - message.setType(Message.TYPE_IMAGE); - message.setRelativeFilePath(message.getUuid()+"."+extension); - } else { - message.setType(Message.TYPE_FILE); - } - if (filename[filename.length - 1].equals("otr")) { - message.setEncryption(Message.ENCRYPTION_OTR); - } else { - message.setEncryption(Message.ENCRYPTION_PGP); - } - } - } else { - message.setType(Message.TYPE_FILE); - } - if (message.getType() == Message.TYPE_FILE) { - String suffix = ""; - if (!fileNameElement.getContent().isEmpty()) { - String parts[] = fileNameElement.getContent().split("/"); - suffix = parts[parts.length - 1]; - if (message.getEncryption() == Message.ENCRYPTION_OTR && suffix.endsWith(".otr")) { - suffix = suffix.substring(0,suffix.length() - 4); - } else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) { - suffix = suffix.substring(0,suffix.length() - 4); - } - } - message.setRelativeFilePath(message.getUuid()+"_"+suffix); - } - long size = Long.parseLong(fileSize.getContent()); - message.setBody(Long.toString(size)); - conversation.add(message); - mXmppConnectionService.updateConversationUi(); - if (size <= this.mJingleConnectionManager.getAutoAcceptFileSize() - && mXmppConnectionService.isDownloadAllowedInConnection()) { - 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.fail(); - return; - } else { - this.file.setKey(key); - } - } - this.file.setExpectedSize(size); - } else { - this.sendCancel(); - this.fail(); - } - } else { - this.sendCancel(); - this.fail(); - } - } - - private void sendInitRequest() { - JinglePacket packet = this.bootstrapPacket("session-initiate"); - Content content = new Content(this.contentCreator, this.contentName); - if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { - 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,new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { - mJingleStatus = JINGLE_STATUS_INITIATED; - mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED); - } else { - fail(); - } - } - }); - - } - } - - 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 && candidate != null && !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.getJid()); - packet.setTo(this.message.getCounterpart()); - packet.setSessionId(this.sessionId); - packet.setInitiator(this.initiator); - return packet; - } - - private void sendJinglePacket(JinglePacket packet) { - account.getXmppConnection().sendIqPacket(packet, responseListener); - } - - private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) { - account.getXmppConnection().sendIqPacket(packet,callback); - } - - 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.fail(); - } - } - 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.disconnectSocks5Connections(); - if (this.initiator.equals(account.getJid())) { - 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().toString()); - 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.getJid())) { - 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.getJid())) { - 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.disconnectSocks5Connections(); - 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, 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, 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_RECEIVED); - this.disconnectSocks5Connections(); - if (this.transport != null && this.transport instanceof JingleInbandTransport) { - this.transport.disconnect(); - } - this.message.setDownloadable(null); - this.mJingleConnectionManager.finishConnection(this); - } - - public void cancel() { - this.disconnectSocks5Connections(); - if (this.transport != null && this.transport instanceof JingleInbandTransport) { - this.transport.disconnect(); - } - this.sendCancel(); - this.mJingleConnectionManager.finishConnection(this); - if (this.responder.equals(account.getJid())) { - this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); - if (this.file!=null) { - file.delete(); - } - this.mXmppConnectionService.updateConversationUi(); - } else { - this.mXmppConnectionService.markMessage(this.message, - Message.STATUS_SEND_FAILED); - this.message.setDownloadable(null); - } - } - - private void fail() { - this.mJingleStatus = JINGLE_STATUS_FAILED; - this.disconnectSocks5Connections(); - if (this.transport != null && this.transport instanceof JingleInbandTransport) { - this.transport.disconnect(); - } - if (this.message != null) { - if (this.responder.equals(account.getJid())) { - this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); - if (this.file!=null) { - file.delete(); - } - this.mXmppConnectionService.updateConversationUi(); - } else { - this.mXmppConnectionService.markMessage(this.message, - Message.STATUS_SEND_FAILED); - this.message.setDownloadable(null); - } - } - 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 disconnectSocks5Connections() { - 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 Jid getInitiator() { - return this.initiator; - } - - public Jid 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; - } - - public void updateProgress(int i) { - this.mProgress = i; - if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { - this.mLastGuiRefresh = SystemClock.elapsedRealtime(); - mXmppConnectionService.updateConversationUi(); - } - } - - 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.State.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; - } - } - - @Override - public int getProgress() { - return this.mProgress; - } - - @Override - public String getMimeType() { - if (this.message.getType() == Message.TYPE_FILE) { - String mime = null; - String path = this.message.getRelativeFilePath(); - if (path != null && !this.message.getRelativeFilePath().isEmpty()) { - mime = URLConnection.guessContentTypeFromName(this.message.getRelativeFilePath()); - if (mime!=null) { - return mime; - } else { - return ""; - } - } else { - return ""; - } - } else { - return "image/webp"; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java deleted file mode 100644 index 5dfa3ff4..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ /dev/null @@ -1,164 +0,0 @@ -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.utils.Xmlns; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; -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; - } - } - IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("item-not-found", - "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); - account.getXmppConnection().sendIqPacket(response, null); - } - } - - public JingleConnection createNewConnection(Message message) { - JingleConnection connection = new JingleConnection(this); - connection.init(message); - this.connections.add(connection); - return connection; - } - - public JingleConnection createNewConnection(final 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 (Config.NO_PROXY_LOOKUP) { - listener.onPrimaryCandidateFound(false, null); - return; - } - if (!this.primaryCandidates.containsKey(account.getJid().toBareJid())) { - final String proxy = account.getXmppConnection().findDiscoItemByFeature(Xmlns.BYTE_STREAMS); - if (proxy != null) { - IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setAttribute("to", proxy); - iq.query(Xmlns.BYTE_STREAMS); - account.getXmppConnection().sendIqPacket(iq,new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Element streamhost = packet.query().findChild("streamhost",Xmlns.BYTE_STREAMS); - final String host = streamhost == null ? null : streamhost.getAttribute("host"); - final String port = streamhost == null ? null : streamhost.getAttribute("port"); - if (host != null && port != null) { - try { - JingleCandidate candidate = new JingleCandidate(nextRandomId(), true); - candidate.setHost(host); - candidate.setPort(Integer.parseInt(port)); - candidate.setType(JingleCandidate.TYPE_PROXY); - candidate.setJid(Jid.fromString(proxy)); - candidate.setPriority(655360 + 65535); - primaryCandidates.put(account.getJid().toBareJid(),candidate); - listener.onPrimaryCandidateFound(true,candidate); - } catch (final NumberFormatException | InvalidJidException e) { - listener.onPrimaryCandidateFound(false,null); - return; - } - } else { - listener.onPrimaryCandidateFound(false,null); - } - } - }); - } else { - listener.onPrimaryCandidateFound(false, null); - } - - } else { - listener.onPrimaryCandidateFound(true, - this.primaryCandidates.get(account.getJid().toBareJid())); - } - } - - 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,"couldn't deliver payload: " + payload.toString()); - } else { - Log.d(Config.LOGTAG, "no sid found in incoming ibb packet"); - } - } - - public void cancelInTransmission() { - for (JingleConnection connection : this.connections) { - if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { - connection.cancel(); - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java deleted file mode 100644 index 174f70fa..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ /dev/null @@ -1,224 +0,0 @@ -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.jid.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public class JingleInbandTransport extends JingleTransport { - - private Account account; - private Jid counterpart; - private int blockSize; - private int bufferSize; - private int seq = 0; - private String sessionId; - - private boolean established = false; - - private boolean connected = true; - - private DownloadableFile file; - private JingleConnection connection; - - private InputStream fileInputStream = null; - private OutputStream fileOutputStream = null; - private long remainingSize = 0; - private long fileSize = 0; - private MessageDigest digest; - - private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; - - private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (connected && packet.getType() == IqPacket.TYPE.RESULT) { - sendNextBlock(); - } - } - }; - - public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) { - this.connection = connection; - this.account = connection.getAccount(); - this.counterpart = connection.getCounterPart(); - 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.connected = true; - 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 = this.fileSize = file.getExpectedSize(); - } catch (final NoSuchAlgorithmException | IOException e) { - callback.onFileTransferAborted(); - } - } - - @Override - public void send(DownloadableFile file, - OnFileTransmissionStatusChanged callback) { - this.onFileTransmissionStatusChanged = callback; - this.file = file; - try { - this.remainingSize = this.file.getSize(); - this.fileSize = this.remainingSize; - this.digest = MessageDigest.getInstance("SHA-1"); - this.digest.reset(); - fileInputStream = this.file.createInputStream(); - if (fileInputStream == null) { - callback.onFileTransferAborted(); - return; - } - if (this.connected) { - this.sendNextBlock(); - } - } catch (NoSuchAlgorithmException e) { - callback.onFileTransferAborted(); - } - } - - @Override - public void disconnect() { - this.connected = false; - if (this.fileOutputStream != null) { - try { - this.fileOutputStream.close(); - } catch (IOException e) { - - } - } - if (this.fileInputStream != null) { - try { - this.fileInputStream.close(); - } catch (IOException e) { - - } - } - } - - 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.remainingSize -= count; - 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++; - connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); - } - } 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); - } else { - connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); - } - } catch (IOException e) { - this.onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } - - public void deliverPayload(IqPacket packet, Element payload) { - if (payload.getName().equals("open")) { - if (!established) { - established = true; - connected = true; - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - } else { - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.ERROR), null); - } - } else if (connected && payload.getName().equals("data")) { - this.receiveNextBlock(payload.getContent()); - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - } else { - // TODO some sort of exception - } - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java deleted file mode 100644 index c3419580..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ /dev/null @@ -1,234 +0,0 @@ -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 JingleConnection connection; - 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; - this.connection = jingleConnection; - try { - MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); - StringBuilder destBuilder = new StringBuilder(); - destBuilder.append(jingleConnection.getSessionId()); - if (candidate.isOurs()) { - destBuilder.append(jingleConnection.getAccount().getJid()); - destBuilder.append(jingleConnection.getCounterPart()); - } else { - destBuilder.append(jingleConnection.getCounterPart()); - destBuilder.append(jingleConnection.getAccount().getJid()); - } - 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; - } - long size = file.getSize(); - long transmitted = 0; - int count; - byte[] buffer = new byte[8192]; - while ((count = fileInputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, count); - digest.update(buffer, 0, count); - transmitted += count; - connection.updateProgress((int) ((((double) transmitted) / size) * 100)); - } - 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; - } - double size = file.getExpectedSize(); - 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; - } - connection.updateProgress((int) (((size - remainingSize) / size) * 100)); - } - 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.outputStream != null) { - try { - this.outputStream.close(); - } catch (IOException e) { - - } - } - if (this.inputStream != null) { - try { - this.inputStream.close(); - } catch (IOException e) { - - } - } - 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/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java deleted file mode 100644 index e832d3f5..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ /dev/null @@ -1,15 +0,0 @@ -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); - - public abstract void disconnect(); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java deleted file mode 100644 index e45e7441..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java +++ /dev/null @@ -1,9 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java deleted file mode 100644 index 2aaf62a1..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java +++ /dev/null @@ -1,9 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java deleted file mode 100644 index 03a437b2..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java +++ /dev/null @@ -1,6 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -public interface OnPrimaryCandidateFound { - public void onPrimaryCandidateFound(boolean success, - JingleCandidate canditate); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java deleted file mode 100644 index 38f03c5d..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -public interface OnTransportConnected { - public void failed(); - - public void established(); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java deleted file mode 100644 index bcadbe77..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ /dev/null @@ -1,102 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java deleted file mode 100644 index 4f73a83a..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ /dev/null @@ -1,96 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle.stanzas; - -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.Jid; -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(final Jid initiator) { - this.jingle.setAttribute("initiator", initiator.toString()); - } - - public boolean isAction(String action) { - return action.equalsIgnoreCase(this.getAction()); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java deleted file mode 100644 index 610d5e76..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ /dev/null @@ -1,13 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java deleted file mode 100644 index 9f5ac988..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ /dev/null @@ -1,73 +0,0 @@ -package eu.siacs.conversations.xmpp.pep; - -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.Jid; - -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 Jid 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/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java deleted file mode 100644 index 55256ece..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java +++ /dev/null @@ -1,54 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.Jid; - -public class AbstractStanza extends Element { - - protected AbstractStanza(final String name) { - super(name); - } - - public Jid getTo() { - return getAttributeAsJid("to"); - } - - public Jid getFrom() { - return getAttributeAsJid("from"); - } - - public String getId() { - return this.getAttribute("id"); - } - - public void setTo(final Jid to) { - if (to != null) { - setAttribute("to", to.toString()); - } - } - - public void setFrom(final Jid from) { - if (from != null) { - setAttribute("from", from.toString()); - } - } - - public void setId(final String id) { - setAttribute("id", id); - } - - public boolean fromServer(final Account account) { - return getFrom() == null - || getFrom().equals(account.getServer()) - || getFrom().equals(account.getJid().toBareJid()) - || getFrom().equals(account.getJid()); - } - - public boolean toServer(final Account account) { - return getTo() == null - || getTo().equals(account.getServer()) - || getTo().equals(account.getJid().toBareJid()) - || getTo().equals(account.getJid()); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java deleted file mode 100644 index 7b36fc49..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ /dev/null @@ -1,63 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -import eu.siacs.conversations.xml.Element; - -public class IqPacket extends AbstractStanza { - - public static enum TYPE { - ERROR, - SET, - RESULT, - GET, - INVALID - } - - public IqPacket(final TYPE type) { - super("iq"); - if (type != TYPE.INVALID) { - this.setAttribute("type", type.toString().toLowerCase()); - } - } - - public IqPacket() { - super("iq"); - } - - public Element query() { - Element query = findChild("query"); - if (query == null) { - query = addChild("query"); - } - return query; - } - - public Element query(final String xmlns) { - final Element query = query(); - query.setAttribute("xmlns", xmlns); - return query(); - } - - public TYPE getType() { - final String type = getAttribute("type"); - switch (type) { - case "error": - return TYPE.ERROR; - case "result": - return TYPE.RESULT; - case "set": - return TYPE.SET; - case "get": - return TYPE.GET; - default: - return TYPE.INVALID; - } - } - - public IqPacket generateResponse(final TYPE type) { - final IqPacket packet = new IqPacket(type); - packet.setTo(this.getFrom()); - packet.setId(this.getId()); - return packet; - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java deleted file mode 100644 index 93aaa68c..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ /dev/null @@ -1,69 +0,0 @@ -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(0, 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; - case TYPE_ERROR: - this.setAttribute("type","error"); - 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/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java deleted file mode 100644 index 7ea32099..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -public class PresencePacket extends AbstractStanza { - - public PresencePacket() { - super("presence"); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java deleted file mode 100644 index 78ab66d8..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java +++ /dev/null @@ -1,10 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java deleted file mode 100644 index f109280f..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java +++ /dev/null @@ -1,10 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java deleted file mode 100644 index f93b5d87..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java +++ /dev/null @@ -1,13 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java deleted file mode 100644 index 78cd81ed..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java +++ /dev/null @@ -1,13 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java deleted file mode 100644 index 98cfc748..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java +++ /dev/null @@ -1,12 +0,0 @@ -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/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java deleted file mode 100644 index 9cdcfa5e..00000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java +++ /dev/null @@ -1,14 +0,0 @@ -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/src/main/res/layout/activity_about.xml b/src/main/res/layout/activity_about.xml index ab0e34eb..922da172 100644 --- a/src/main/res/layout/activity_about.xml +++ b/src/main/res/layout/activity_about.xml @@ -1,6 +1,6 @@ diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index b343781b..361f1fff 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -29,7 +29,7 @@ android:layout_alignParentLeft="true" android:background="@color/primarybackground" > - - + - - - -- cgit v1.2.3 From b6584dfd0b3a7347c7788143fda486f18a1518ee Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 26 May 2015 22:28:31 +0200 Subject: Status color added to "create conversation" (refers to ticket #22 in flyspray) --- .../thedevstack/conversationsplus/entities/Bookmark.java | 7 +++++++ .../thedevstack/conversationsplus/entities/Contact.java | 6 ++++++ .../thedevstack/conversationsplus/entities/ListItem.java | 2 ++ .../conversationsplus/ui/adapter/ListItemAdapter.java | 4 ++++ .../de/thedevstack/conversationsplus/utils/UIHelper.java | 15 +++++++++++++++ src/main/res/layout/contact.xml | 11 +++++++++-- 6 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java index ea7df2f2..8a6b1405 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java @@ -1,5 +1,7 @@ package de.thedevstack.conversationsplus.entities; +import android.graphics.Color; + import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -74,6 +76,11 @@ public class Bookmark extends Element implements ListItem { return tags; } + @Override + public int getStatusColor() { + return Color.parseColor("#259B23"); + } + public String getNick() { Element nick = this.findChild("nick"); if (nick != null) { diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java index 07d42d28..c4f1c9eb 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java @@ -2,6 +2,7 @@ package de.thedevstack.conversationsplus.entities; import android.content.ContentValues; import android.database.Cursor; +import android.graphics.Color; import org.json.JSONArray; import org.json.JSONException; @@ -147,6 +148,11 @@ public class Contact implements ListItem, Blockable { return tags; } + @Override + public int getStatusColor() { + return Color.parseColor(UIHelper.getStatusColor(getMostAvailableStatus())); + } + public boolean match(String needle) { if (needle == null || needle.isEmpty()) { return true; diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java index 825eb481..9daf90d1 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java @@ -11,6 +11,8 @@ public interface ListItem extends Comparable { public List getTags(); + public int getStatusColor(); + public final class Tag { private final String name; private final int color; diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java index e78d7f5c..c453416a 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java @@ -57,6 +57,10 @@ public class ListItemAdapter extends ArrayAdapter { if (view == null) { view = inflater.inflate(R.layout.contact, parent, false); } + + TextView tvStatus = (TextView) view.findViewById(R.id.contact_status); + tvStatus.setBackgroundColor(item.getStatusColor()); + TextView tvName = (TextView) view.findViewById(R.id.contact_display_name); TextView tvJid = (TextView) view.findViewById(R.id.contact_jid); ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java index bda3f770..3edaad0d 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java @@ -15,6 +15,7 @@ import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Downloadable; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.Presences; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import android.content.Context; @@ -354,6 +355,20 @@ public class UIHelper { } } + public static String getStatusColor(int status) { + switch (status) { + case Presences.ONLINE: + case Presences.CHAT: + return "#259B23"; + case Presences.AWAY: + case Presences.XA: + return "#FF9800"; + case Presences.DND: + return "#E51C23"; + } + return "#CCCCCC"; + } + private static String getDisplayedMucCounterpart(final Jid counterpart) { if (counterpart==null) { return ""; diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml index 69545c25..f0ef5d94 100644 --- a/src/main/res/layout/contact.xml +++ b/src/main/res/layout/contact.xml @@ -11,8 +11,15 @@ android:layout_height="48dp" android:layout_alignParentLeft="true" android:scaleType="centerCrop" - android:src="@drawable/ic_profile" > - + android:src="@drawable/ic_profile" /> + Date: Tue, 26 May 2015 22:30:41 +0200 Subject: Show Online status in contact list with respect to SHOW_ONLINE_STATUS preference --- .../thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java index c453416a..4b70417e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ListItemAdapter.java @@ -9,6 +9,7 @@ import de.thedevstack.conversationsplus.entities.ListItem; import de.thedevstack.conversationsplus.ui.XmppActivity; import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.tzur.conversations.Settings; import android.content.Context; import android.content.SharedPreferences; @@ -58,8 +59,10 @@ public class ListItemAdapter extends ArrayAdapter { view = inflater.inflate(R.layout.contact, parent, false); } - TextView tvStatus = (TextView) view.findViewById(R.id.contact_status); - tvStatus.setBackgroundColor(item.getStatusColor()); + if (Settings.SHOW_ONLINE_STATUS) { + TextView tvStatus = (TextView) view.findViewById(R.id.contact_status); + tvStatus.setBackgroundColor(item.getStatusColor()); + } TextView tvName = (TextView) view.findViewById(R.id.contact_display_name); TextView tvJid = (TextView) view.findViewById(R.id.contact_jid); -- cgit v1.2.3