aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore97
-rw-r--r--CHANGELOG.md99
-rw-r--r--LICENCE4
-rw-r--r--LICENCE_APACHE178
-rw-r--r--LICENCE_LGPL2.1503
-rw-r--r--LICENCE_WTFPL13
-rw-r--r--LICENSE674
-rw-r--r--README.md301
-rw-r--r--art/LICENSE425
-rw-r--r--art/conversations.svg381
-rw-r--r--art/conversations_baloon.svg422
-rw-r--r--art/conversations_mono.svg400
-rw-r--r--art/ic_action_send_now.svg69
-rw-r--r--art/ic_received_indicator.svg76
-rw-r--r--art/ic_secure_indicator.xcfbin0 -> 1240 bytes
-rw-r--r--art/logo.pngbin0 -> 53734 bytes
-rwxr-xr-xart/render.rb22
-rw-r--r--build.gradle165
-rw-r--r--gradle.properties.example21
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew164
-rw-r--r--gradlew.bat90
-rw-r--r--libs/MemorizingTrustManager/.gitignore11
-rw-r--r--libs/MemorizingTrustManager/AndroidManifest.xml11
-rw-r--r--libs/MemorizingTrustManager/LICENSE.txt21
-rw-r--r--libs/MemorizingTrustManager/README.mdwn125
-rw-r--r--libs/MemorizingTrustManager/ant.properties17
-rw-r--r--libs/MemorizingTrustManager/build.gradle32
-rw-r--r--libs/MemorizingTrustManager/build.xml92
-rw-r--r--libs/MemorizingTrustManager/example/AndroidManifest.xml29
-rw-r--r--libs/MemorizingTrustManager/example/ant.properties18
-rw-r--r--libs/MemorizingTrustManager/example/build.gradle23
-rw-r--r--libs/MemorizingTrustManager/example/build.xml92
-rw-r--r--libs/MemorizingTrustManager/example/proguard-project.txt20
-rw-r--r--libs/MemorizingTrustManager/example/project.properties12
-rw-r--r--libs/MemorizingTrustManager/example/res/layout/mtmexample.xml36
-rw-r--r--libs/MemorizingTrustManager/example/res/values/strings.xml4
-rw-r--r--libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/JULHandler.java169
-rw-r--r--libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/MTMExample.java143
-rw-r--r--libs/MemorizingTrustManager/libs/.android_sucks0
-rw-r--r--libs/MemorizingTrustManager/mtm-notification.pngbin0 -> 59399 bytes
-rw-r--r--libs/MemorizingTrustManager/mtm-screenshot.pngbin0 -> 85104 bytes
-rw-r--r--libs/MemorizingTrustManager/mtm-servername.pngbin0 -> 83611 bytes
-rw-r--r--libs/MemorizingTrustManager/proguard-project.txt20
-rw-r--r--libs/MemorizingTrustManager/project.properties12
-rw-r--r--libs/MemorizingTrustManager/res/values-de/strings.xml17
-rw-r--r--libs/MemorizingTrustManager/res/values-es/strings.xml17
-rw-r--r--libs/MemorizingTrustManager/res/values-eu/strings.xml17
-rw-r--r--libs/MemorizingTrustManager/res/values-fi/strings.xml16
-rw-r--r--libs/MemorizingTrustManager/res/values-fr/strings.xml16
-rw-r--r--libs/MemorizingTrustManager/res/values-no/strings.xml16
-rw-r--r--libs/MemorizingTrustManager/res/values/strings.xml17
-rw-r--r--libs/MemorizingTrustManager/settings.gradle1
-rw-r--r--libs/MemorizingTrustManager/src/de/duenndns/ssl/MTMDecision.java33
-rw-r--r--libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingActivity.java103
-rw-r--r--libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java735
m---------libs/minidns6
m---------libs/openpgp-api-lib6
-rw-r--r--proguard-rules.txt27
-rw-r--r--screenshots.pngbin0 -> 1035568 bytes
-rw-r--r--settings.gradle5
-rw-r--r--src/main/AndroidManifest.xml118
-rw-r--r--src/main/java/de/measite/minidns/Client.java323
-rw-r--r--src/main/java/de/measite/minidns/DNSCache.java23
-rw-r--r--src/main/java/de/measite/minidns/DNSMessage.java524
-rw-r--r--src/main/java/de/measite/minidns/LRUCache.java139
-rw-r--r--src/main/java/de/measite/minidns/Question.java158
-rw-r--r--src/main/java/de/measite/minidns/Record.java343
-rw-r--r--src/main/java/de/measite/minidns/record/A.java43
-rw-r--r--src/main/java/de/measite/minidns/record/AAAA.java49
-rw-r--r--src/main/java/de/measite/minidns/record/CNAME.java46
-rw-r--r--src/main/java/de/measite/minidns/record/Data.java34
-rw-r--r--src/main/java/de/measite/minidns/record/NS.java15
-rw-r--r--src/main/java/de/measite/minidns/record/PTR.java19
-rw-r--r--src/main/java/de/measite/minidns/record/SRV.java124
-rw-r--r--src/main/java/de/measite/minidns/record/TXT.java65
-rw-r--r--src/main/java/de/measite/minidns/util/NameUtil.java129
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java25
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/OtrEngine.java231
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpEngine.java385
-rw-r--r--src/main/java/eu/siacs/conversations/entities/AbstractEntity.java21
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Account.java399
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Bookmark.java137
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Contact.java367
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java500
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Downloadable.java21
-rw-r--r--src/main/java/eu/siacs/conversations/entities/DownloadableFile.java154
-rw-r--r--src/main/java/eu/siacs/conversations/entities/ListItem.java7
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java478
-rw-r--r--src/main/java/eu/siacs/conversations/entities/MucOptions.java369
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Presences.java76
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Roster.java83
-rw-r--r--src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java48
-rw-r--r--src/main/java/eu/siacs/conversations/generator/IqGenerator.java96
-rw-r--r--src/main/java/eu/siacs/conversations/generator/MessageGenerator.java178
-rw-r--r--src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java57
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpConnection.java255
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java28
-rw-r--r--src/main/java/eu/siacs/conversations/parser/AbstractParser.java92
-rw-r--r--src/main/java/eu/siacs/conversations/parser/IqParser.java92
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java517
-rw-r--r--src/main/java/eu/siacs/conversations/parser/PresenceParser.java133
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java335
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/FileBackend.java480
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java5
-rw-r--r--src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java23
-rw-r--r--src/main/java/eu/siacs/conversations/services/AvatarService.java298
-rw-r--r--src/main/java/eu/siacs/conversations/services/EventReceiver.java24
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java237
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java1927
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java145
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java280
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java436
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java947
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java781
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java423
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditMessage.java39
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java217
-rw-r--r--src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java242
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsActivity.java74
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsFragment.java15
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java185
-rw-r--r--src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java677
-rw-r--r--src/main/java/eu/siacs/conversations/ui/UiCallback.java11
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java637
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java102
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java135
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java74
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java44
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java560
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java112
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java185
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java44
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java117
-rw-r--r--src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java9
-rw-r--r--src/main/java/eu/siacs/conversations/utils/PRNGFixes.java327
-rw-r--r--src/main/java/eu/siacs/conversations/utils/PhoneHelper.java95
-rw-r--r--src/main/java/eu/siacs/conversations/utils/UIHelper.java225
-rw-r--r--src/main/java/eu/siacs/conversations/utils/Validator.java14
-rw-r--r--src/main/java/eu/siacs/conversations/utils/XmlHelper.java12
-rw-r--r--src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java54
-rw-r--r--src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java95
-rw-r--r--src/main/java/eu/siacs/conversations/xml/Element.java148
-rw-r--r--src/main/java/eu/siacs/conversations/xml/Tag.java104
-rw-r--r--src/main/java/eu/siacs/conversations/xml/TagWriter.java114
-rw-r--r--src/main/java/eu/siacs/conversations/xml/XmlReader.java141
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java5
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java1130
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java143
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java910
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java163
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java191
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java212
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java6
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java102
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java95
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java71
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java34
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java76
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java66
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java10
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java10
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java12
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java14
-rw-r--r--src/main/res/drawable-hdpi/ic_action_add_group.pngbin0 -> 876 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_add_person.pngbin0 -> 616 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_chat.pngbin0 -> 295 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_copy.pngbin0 -> 381 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_discard.pngbin0 -> 450 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_edit.pngbin0 -> 765 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_edit_dark.pngbin0 -> 884 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_group.pngbin0 -> 776 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_new.pngbin0 -> 262 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_new_attachment.pngbin0 -> 587 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_not_secure.pngbin0 -> 367 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_refresh.pngbin0 -> 678 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_remove.pngbin0 -> 448 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_search.pngbin0 -> 650 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_secure.pngbin0 -> 384 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_send_now_away.pngbin0 -> 932 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_send_now_dnd.pngbin0 -> 1135 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_send_now_offline.pngbin0 -> 767 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_action_send_now_online.pngbin0 -> 1095 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_activity.pngbin0 -> 3040 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_indicator.pngbin0 -> 684 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_launcher.pngbin0 -> 4416 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_notification.pngbin0 -> 1033 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_profile.pngbin0 -> 999 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_received_indicator.pngbin0 -> 686 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_secure_indicator.pngbin0 -> 294 bytes
-rw-r--r--src/main/res/drawable-hdpi/tab_selected_conversations.9.pngbin0 -> 99 bytes
-rw-r--r--src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.pngbin0 -> 99 bytes
-rw-r--r--src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.pngbin0 -> 105 bytes
-rw-r--r--src/main/res/drawable-hdpi/tab_unselected_conversations.9.pngbin0 -> 101 bytes
-rw-r--r--src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.pngbin0 -> 93 bytes
-rw-r--r--src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.pngbin0 -> 100 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_add_group.pngbin0 -> 634 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_add_person.pngbin0 -> 469 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_chat.pngbin0 -> 261 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_copy.pngbin0 -> 288 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_discard.pngbin0 -> 324 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_edit.pngbin0 -> 522 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_edit_dark.pngbin0 -> 587 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_group.pngbin0 -> 546 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_new.pngbin0 -> 185 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_new_attachment.pngbin0 -> 415 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_not_secure.pngbin0 -> 298 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_refresh.pngbin0 -> 507 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_remove.pngbin0 -> 282 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_search.pngbin0 -> 449 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_secure.pngbin0 -> 304 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_send_now_away.pngbin0 -> 650 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_send_now_dnd.pngbin0 -> 784 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_send_now_offline.pngbin0 -> 535 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_action_send_now_online.pngbin0 -> 779 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_activity.pngbin0 -> 1854 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_indicator.pngbin0 -> 490 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_launcher.pngbin0 -> 2726 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_notification.pngbin0 -> 681 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_profile.pngbin0 -> 622 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_received_indicator.pngbin0 -> 447 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_secure_indicator.pngbin0 -> 295 bytes
-rw-r--r--src/main/res/drawable-mdpi/tab_selected_conversations.9.pngbin0 -> 96 bytes
-rw-r--r--src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.pngbin0 -> 96 bytes
-rw-r--r--src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.pngbin0 -> 102 bytes
-rw-r--r--src/main/res/drawable-mdpi/tab_unselected_conversations.9.pngbin0 -> 105 bytes
-rw-r--r--src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.pngbin0 -> 90 bytes
-rw-r--r--src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.pngbin0 -> 97 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_add_group.pngbin0 -> 1122 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_add_person.pngbin0 -> 798 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_chat.pngbin0 -> 310 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_copy.pngbin0 -> 353 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_discard.pngbin0 -> 543 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_edit.pngbin0 -> 994 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_edit_dark.pngbin0 -> 1179 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_group.pngbin0 -> 1048 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_new.pngbin0 -> 234 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_new_attachment.pngbin0 -> 753 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_not_secure.pngbin0 -> 482 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_refresh.pngbin0 -> 901 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_remove.pngbin0 -> 513 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_search.pngbin0 -> 827 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_secure.pngbin0 -> 468 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_send_now_away.pngbin0 -> 1180 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_send_now_dnd.pngbin0 -> 1438 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_send_now_offline.pngbin0 -> 968 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_action_send_now_online.pngbin0 -> 1395 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_activity.pngbin0 -> 4349 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_indicator.pngbin0 -> 915 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_launcher.pngbin0 -> 6503 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_notification.pngbin0 -> 1407 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_profile.pngbin0 -> 1374 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_received_indicator.pngbin0 -> 855 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_secure_indicator.pngbin0 -> 410 bytes
-rw-r--r--src/main/res/drawable-xhdpi/tab_selected_conversations.9.pngbin0 -> 104 bytes
-rw-r--r--src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.pngbin0 -> 103 bytes
-rw-r--r--src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.pngbin0 -> 110 bytes
-rw-r--r--src/main/res/drawable-xhdpi/tab_unselected_conversations.9.pngbin0 -> 112 bytes
-rw-r--r--src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.pngbin0 -> 93 bytes
-rw-r--r--src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.pngbin0 -> 101 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_add_group.pngbin0 -> 1643 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_add_person.pngbin0 -> 1088 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_chat.pngbin0 -> 383 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_copy.pngbin0 -> 470 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_discard.pngbin0 -> 765 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_edit.pngbin0 -> 1458 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_edit_dark.pngbin0 -> 1670 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_group.pngbin0 -> 1475 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_new.pngbin0 -> 288 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_new_attachment.pngbin0 -> 1048 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_not_secure.pngbin0 -> 593 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_refresh.pngbin0 -> 1274 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_remove.pngbin0 -> 681 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_search.pngbin0 -> 1152 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_secure.pngbin0 -> 586 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_send_now_away.pngbin0 -> 1426 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.pngbin0 -> 1456 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_send_now_offline.pngbin0 -> 1433 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_action_send_now_online.pngbin0 -> 1458 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_activity.pngbin0 -> 7209 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_indicator.pngbin0 -> 1298 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 11054 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_notification.pngbin0 -> 2250 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_profile.pngbin0 -> 2137 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_received_indicator.pngbin0 -> 1236 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_secure_indicator.pngbin0 -> 380 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/tab_selected_conversations.9.pngbin0 -> 108 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.pngbin0 -> 108 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.pngbin0 -> 114 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.pngbin0 -> 109 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.pngbin0 -> 95 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.pngbin0 -> 102 bytes
-rw-r--r--src/main/res/drawable/actionbar_tab_indicator.xml21
-rw-r--r--src/main/res/drawable/es_slidingpane_shadow.xml12
-rw-r--r--src/main/res/drawable/grey.xml7
-rw-r--r--src/main/res/drawable/greybackground.xml6
-rw-r--r--src/main/res/drawable/infocard_border.xml19
-rw-r--r--src/main/res/drawable/message_border.xml15
-rw-r--r--src/main/res/drawable/snackbar.xml14
-rw-r--r--src/main/res/layout-w360dp/fragment_conversations_overview.xml30
-rw-r--r--src/main/res/layout-w384dp/fragment_conversations_overview.xml30
-rw-r--r--src/main/res/layout-w600dp/fragment_conversations_overview.xml30
-rw-r--r--src/main/res/layout-w960dp/fragment_conversations_overview.xml32
-rw-r--r--src/main/res/layout/account_row.xml43
-rw-r--r--src/main/res/layout/actionview_search.xml19
-rw-r--r--src/main/res/layout/activity_choose_contact.xml13
-rw-r--r--src/main/res/layout/activity_contact_details.xml114
-rw-r--r--src/main/res/layout/activity_edit_account.xml272
-rw-r--r--src/main/res/layout/activity_muc_details.xml119
-rw-r--r--src/main/res/layout/activity_publish_profile_picture.xml106
-rw-r--r--src/main/res/layout/activity_start_conversation.xml8
-rw-r--r--src/main/res/layout/contact.xml51
-rw-r--r--src/main/res/layout/contact_key.xml41
-rw-r--r--src/main/res/layout/conversation_list_row.xml68
-rw-r--r--src/main/res/layout/create_contact_dialog.xml39
-rw-r--r--src/main/res/layout/dialog_clear_history.xml21
-rw-r--r--src/main/res/layout/dialog_verify_otr.xml60
-rw-r--r--src/main/res/layout/fragment_conversation.xml102
-rw-r--r--src/main/res/layout/fragment_conversations_overview.xml30
-rw-r--r--src/main/res/layout/join_conference_dialog.xml47
-rw-r--r--src/main/res/layout/manage_accounts.xml16
-rw-r--r--src/main/res/layout/message_null.xml7
-rw-r--r--src/main/res/layout/message_received.xml97
-rw-r--r--src/main/res/layout/message_sent.xml108
-rw-r--r--src/main/res/layout/message_status.xml22
-rw-r--r--src/main/res/layout/quickedit.xml19
-rw-r--r--src/main/res/layout/share_with.xml13
-rw-r--r--src/main/res/menu/attachment_choices.xml15
-rw-r--r--src/main/res/menu/choose_contact.xml11
-rw-r--r--src/main/res/menu/conference_context.xml11
-rw-r--r--src/main/res/menu/contact_context.xml14
-rw-r--r--src/main/res/menu/contact_details.xml27
-rw-r--r--src/main/res/menu/conversations.xml63
-rw-r--r--src/main/res/menu/encryption_choices.xml16
-rw-r--r--src/main/res/menu/manageaccounts.xml15
-rw-r--r--src/main/res/menu/manageaccounts_context.xml21
-rw-r--r--src/main/res/menu/muc_details.xml21
-rw-r--r--src/main/res/menu/share_with.xml11
-rw-r--r--src/main/res/menu/start_conversation.xml31
-rw-r--r--src/main/res/values-ca/arrays.xml24
-rw-r--r--src/main/res/values-ca/strings.xml83
-rw-r--r--src/main/res/values-cs/arrays.xml39
-rw-r--r--src/main/res/values-cs/strings.xml260
-rw-r--r--src/main/res/values-de/arrays.xml31
-rw-r--r--src/main/res/values-de/strings.xml269
-rw-r--r--src/main/res/values-es/arrays.xml39
-rw-r--r--src/main/res/values-es/strings.xml269
-rw-r--r--src/main/res/values-eu/arrays.xml39
-rw-r--r--src/main/res/values-eu/strings.xml276
-rw-r--r--src/main/res/values-fr/arrays.xml24
-rw-r--r--src/main/res/values-fr/strings.xml273
-rw-r--r--src/main/res/values-gl/arrays.xml24
-rw-r--r--src/main/res/values-gl/strings.xml130
-rw-r--r--src/main/res/values-it/arrays.xml39
-rw-r--r--src/main/res/values-it/strings.xml260
-rw-r--r--src/main/res/values-iw/arrays.xml24
-rw-r--r--src/main/res/values-iw/strings.xml224
-rw-r--r--src/main/res/values-nl/arrays.xml24
-rw-r--r--src/main/res/values-nl/strings.xml233
-rw-r--r--src/main/res/values-ru/arrays.xml24
-rw-r--r--src/main/res/values-ru/strings.xml260
-rw-r--r--src/main/res/values-sv/arrays.xml24
-rw-r--r--src/main/res/values-sv/strings.xml260
-rw-r--r--src/main/res/values-zh-rCN/arrays.xml39
-rw-r--r--src/main/res/values-zh-rCN/strings.xml260
-rw-r--r--src/main/res/values-zh-rTW/arrays.xml39
-rw-r--r--src/main/res/values-zh-rTW/strings.xml263
-rw-r--r--src/main/res/values/arrays.xml39
-rw-r--r--src/main/res/values/attrs.xml8
-rw-r--r--src/main/res/values/colors.xml17
-rw-r--r--src/main/res/values/strings.xml276
-rw-r--r--src/main/res/values/styles.xml8
-rw-r--r--src/main/res/values/themes.xml35
-rw-r--r--src/main/res/xml/preferences.xml114
390 files changed, 30719 insertions, 2904 deletions
diff --git a/.gitignore b/.gitignore
index 2362cd05..5b492835 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,14 @@
-# From https://github.com/github/gitignore
+.classpath
+*.swp
+.settings
-# # # # # # # # # # # #
-# Android gitignore #
-# # # # # # # # # # # #
+# https://github.com/github/gitignore/blob/master/Gradle.gitignore
+.gradle/
+build/
+# Ignore Gradle GUI config
+gradle-app.setting
+# https://github.com/github/gitignore/blob/master/Android.gitignore
# Built application files
*.apk
*.ap_
@@ -18,90 +23,16 @@
bin/
gen/
-# Gradle files
-.gradle/
-build/
-
# Local configuration file (sdk path, etc)
local.properties
-gradle.properties
# Proguard folder generated by Eclipse
proguard/
-# # # # # # # #
-# VIM / Linux #
-# # # # # # # #
-
-[._]*.s[a-w][a-z]
-[._]s[a-w][a-z]
-*.un~
-Session.vim
-.netrwhist
-*~
-.directory
-
-# # # # # #
-# Eclipse #
-# # # # # #
-
-*.pydevproject
-.metadata
-.gradle
-bin/
-tmp/
-*.tmp
-*.bak
-*.swp
-*~.nib
-local.properties
-.settings/
-.loadpath
-.classpath
-.project
-
-# External tool builders
-.externalToolBuilders/
-
-# Locally stored "Eclipse launch configurations"
-*.launch
-
-# CDT-specific
-.cproject
-
-# PDT-specific
-.buildpath
-
-# sbteclipse plugin
-.target
-
-# TeXlipse plugin
-.texlipse
-
-# # # # #
-# OS X #
-# # # # #
-
-.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must ends with two \r.
-Icon
-
-
-# Thumbnails
-._*
-
-# Files that might appear on external disk
-.Spotlight-V100
-.Trashes
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
+# Log Files
+*.log
+*.iml
+.idea
+import-summary.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..29277eb4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,99 @@
+###Changelog
+
+####Version 0.7.3
+* revised tablet ui
+* internal rewrites
+* bug fixes
+
+####Version 0.7.2
+* show full timestamp in messages
+* brought back option to use JID to identify conferences
+* optionally request delivery receipts (expert option)
+* more languages
+* bug fixes
+
+####Version 0.7.1
+* Optionally use send button as status indicator
+
+####Version 0.7
+* Ability to disable notifications for single conversations
+* Merge messages in chat bubbles
+* Fixes for OpenPGP and OTR (please republish your public key)
+* Improved reliability on sending messages
+* Join password protected Conferences
+* Configurable font size
+* Expert options for encryption
+
+####Version 0.6
+* Support for server side avatars
+* save images in gallery
+* show contact name and picture in non-anonymous conferences
+* reworked account creation
+* various bug fixes
+
+####Version 0.5.2
+* minor bug fixes
+
+####Version 0.5.1
+* couple of small bug fixes that have been missed in 0.5
+* complete translations for Swedish, Dutch, German, Spanish, French, Russian
+
+####Version 0.5
+* UI overhaul
+* MUC / Conference bookmarks
+* A lot of bug fixes
+
+####Version 0.4
+* OTR file encryption
+* keep OTR messages and files on device until both parties or online at the same time
+* XEP-0333. Mark wether the other party has read your messages
+* Delayed messages are now tagged properly
+* Share images from the Gallery
+* Infinit history scrolling
+* Mark the last used presence in presence selection dialog
+
+####Version 0.3
+* Mostly bug fixes and internal rewrites
+* Touch contact picture in conference to highlight
+* Long press on received image to share
+* made OTR more reliable
+* improved issues with occasional message lost
+* experimental conference encryption. (see FAQ)
+
+####Version 0.2.3
+* regression fix with receiving encrypted images
+
+####Version 0.2.2
+* Ability to take photos directly
+* Improved openPGP offline handling
+* Various bug fixes
+* Updated Translations
+
+####Version 0.2.1
+* Various bug fixes
+* Updated Translations
+
+####Version 0.2
+* Image file transfer
+* Better integration with OpenKeychain (PGP encryption)
+* Nicer conversation tiles for conferences
+* Ability to clear conversation history
+* A lot of bug fixes and code clean up
+
+####Version 0.1.3
+* Switched to minidns library to resolve SRV records
+* Faster DNS in some cases
+* Enabled stream compression
+* Added permanent notification when an account fails to connect
+* Various bug fixes involving message notifications
+* Added support for DIGEST-MD5 auth
+
+####Version 0.1.2
+* Various bug fixes relating to conferences
+* Further DNS lookup improvements
+
+####Version 0.1.1
+* Fixed the 'server not found' bug
+
+####Version 0.1
+* Initial release
diff --git a/LICENCE b/LICENCE
deleted file mode 100644
index 4c2ee7c0..00000000
--- a/LICENCE
+++ /dev/null
@@ -1,4 +0,0 @@
-This software may be used under the terms of (at your choice)
-- LGPL version 2 (or later) (see LICENCE_LGPL2.1 for details)
-- Apache Software licence (see LICENCE_APACHE for details)
-- WTFPL (see LICENCE_WTFPL for details)
diff --git a/LICENCE_APACHE b/LICENCE_APACHE
deleted file mode 100644
index e454a525..00000000
--- a/LICENCE_APACHE
+++ /dev/null
@@ -1,178 +0,0 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
diff --git a/LICENCE_LGPL2.1 b/LICENCE_LGPL2.1
deleted file mode 100644
index 51a70cae..00000000
--- a/LICENCE_LGPL2.1
+++ /dev/null
@@ -1,503 +0,0 @@
- GNU LESSER GENERAL PUBLIC LICENSE
- Version 2.1, February 1999
-
- Copyright (C) 1991, 1999 Free Software Foundation, Inc.
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL. It also counts
- as the successor of the GNU Library Public License, version 2, hence
- the version number 2.1.]
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-Licenses are intended to guarantee your freedom to share and change
-free software--to make sure the software is free for all its users.
-
- This license, the Lesser General Public License, applies to some
-specially designated software packages--typically libraries--of the
-Free Software Foundation and other authors who decide to use it. You
-can use it too, but we suggest you first think carefully about whether
-this license or the ordinary General Public License is the better
-strategy to use in any particular case, based on the explanations below.
-
- When we speak of free software, we are referring to freedom of use,
-not price. Our General Public Licenses are designed to make sure that
-you have the freedom to distribute copies of free software (and charge
-for this service if you wish); that you receive source code or can get
-it if you want it; that you can change the software and use pieces of
-it in new free programs; and that you are informed that you can do
-these things.
-
- To protect your rights, we need to make restrictions that forbid
-distributors to deny you these rights or to ask you to surrender these
-rights. These restrictions translate to certain responsibilities for
-you if you distribute copies of the library or if you modify it.
-
- For example, if you distribute copies of the library, whether gratis
-or for a fee, you must give the recipients all the rights that we gave
-you. You must make sure that they, too, receive or can get the source
-code. If you link other code with the library, you must provide
-complete object files to the recipients, so that they can relink them
-with the library after making changes to the library and recompiling
-it. And you must show them these terms so they know their rights.
-
- We protect your rights with a two-step method: (1) we copyright the
-library, and (2) we offer you this license, which gives you legal
-permission to copy, distribute and/or modify the library.
-
- To protect each distributor, we want to make it very clear that
-there is no warranty for the free library. Also, if the library is
-modified by someone else and passed on, the recipients should know
-that what they have is not the original version, so that the original
-author's reputation will not be affected by problems that might be
-introduced by others.
-
- Finally, software patents pose a constant threat to the existence of
-any free program. We wish to make sure that a company cannot
-effectively restrict the users of a free program by obtaining a
-restrictive license from a patent holder. Therefore, we insist that
-any patent license obtained for a version of the library must be
-consistent with the full freedom of use specified in this license.
-
- Most GNU software, including some libraries, is covered by the
-ordinary GNU General Public License. This license, the GNU Lesser
-General Public License, applies to certain designated libraries, and
-is quite different from the ordinary General Public License. We use
-this license for certain libraries in order to permit linking those
-libraries into non-free programs.
-
- When a program is linked with a library, whether statically or using
-a shared library, the combination of the two is legally speaking a
-combined work, a derivative of the original library. The ordinary
-General Public License therefore permits such linking only if the
-entire combination fits its criteria of freedom. The Lesser General
-Public License permits more lax criteria for linking other code with
-the library.
-
- We call this license the "Lesser" General Public License because it
-does Less to protect the user's freedom than the ordinary General
-Public License. It also provides other free software developers Less
-of an advantage over competing non-free programs. These disadvantages
-are the reason we use the ordinary General Public License for many
-libraries. However, the Lesser license provides advantages in certain
-special circumstances.
-
- For example, on rare occasions, there may be a special need to
-encourage the widest possible use of a certain library, so that it becomes
-a de-facto standard. To achieve this, non-free programs must be
-allowed to use the library. A more frequent case is that a free
-library does the same job as widely used non-free libraries. In this
-case, there is little to gain by limiting the free library to free
-software only, so we use the Lesser General Public License.
-
- In other cases, permission to use a particular library in non-free
-programs enables a greater number of people to use a large body of
-free software. For example, permission to use the GNU C Library in
-non-free programs enables many more people to use the whole GNU
-operating system, as well as its variant, the GNU/Linux operating
-system.
-
- Although the Lesser General Public License is Less protective of the
-users' freedom, it does ensure that the user of a program that is
-linked with the Library has the freedom and the wherewithal to run
-that program using a modified version of the Library.
-
- The precise terms and conditions for copying, distribution and
-modification follow. Pay close attention to the difference between a
-"work based on the library" and a "work that uses the library". The
-former contains code derived from the library, whereas the latter must
-be combined with the library in order to run.
-
- GNU LESSER GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License Agreement applies to any software library or other
-program which contains a notice placed by the copyright holder or
-other authorized party saying it may be distributed under the terms of
-this Lesser General Public License (also called "this License").
-Each licensee is addressed as "you".
-
- A "library" means a collection of software functions and/or data
-prepared so as to be conveniently linked with application programs
-(which use some of those functions and data) to form executables.
-
- The "Library", below, refers to any such software library or work
-which has been distributed under these terms. A "work based on the
-Library" means either the Library or any derivative work under
-copyright law: that is to say, a work containing the Library or a
-portion of it, either verbatim or with modifications and/or translated
-straightforwardly into another language. (Hereinafter, translation is
-included without limitation in the term "modification".)
-
- "Source code" for a work means the preferred form of the work for
-making modifications to it. For a library, complete source code means
-all the source code for all modules it contains, plus any associated
-interface definition files, plus the scripts used to control compilation
-and installation of the library.
-
- Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running a program using the Library is not restricted, and output from
-such a program is covered only if its contents constitute a work based
-on the Library (independent of the use of the Library in a tool for
-writing it). Whether that is true depends on what the Library does
-and what the program that uses the Library does.
-
- 1. You may copy and distribute verbatim copies of the Library's
-complete source code as you receive it, in any medium, provided that
-you conspicuously and appropriately publish on each copy an
-appropriate copyright notice and disclaimer of warranty; keep intact
-all the notices that refer to this License and to the absence of any
-warranty; and distribute a copy of this License along with the
-Library.
-
- You may charge a fee for the physical act of transferring a copy,
-and you may at your option offer warranty protection in exchange for a
-fee.
-
- 2. You may modify your copy or copies of the Library or any portion
-of it, thus forming a work based on the Library, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) The modified work must itself be a software library.
-
- b) You must cause the files modified to carry prominent notices
- stating that you changed the files and the date of any change.
-
- c) You must cause the whole of the work to be licensed at no
- charge to all third parties under the terms of this License.
-
- d) If a facility in the modified Library refers to a function or a
- table of data to be supplied by an application program that uses
- the facility, other than as an argument passed when the facility
- is invoked, then you must make a good faith effort to ensure that,
- in the event an application does not supply such function or
- table, the facility still operates, and performs whatever part of
- its purpose remains meaningful.
-
- (For example, a function in a library to compute square roots has
- a purpose that is entirely well-defined independent of the
- application. Therefore, Subsection 2d requires that any
- application-supplied function or table used by this function must
- be optional: if the application does not supply it, the square
- root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Library,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Library, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote
-it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library
-with the Library (or with a work based on the Library) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may opt to apply the terms of the ordinary GNU General Public
-License instead of this License to a given copy of the Library. To do
-this, you must alter all the notices that refer to this License, so
-that they refer to the ordinary GNU General Public License, version 2,
-instead of to this License. (If a newer version than version 2 of the
-ordinary GNU General Public License has appeared, then you can specify
-that version instead if you wish.) Do not make any other change in
-these notices.
-
- Once this change is made in a given copy, it is irreversible for
-that copy, so the ordinary GNU General Public License applies to all
-subsequent copies and derivative works made from that copy.
-
- This option is useful when you wish to copy part of the code of
-the Library into a program that is not a library.
-
- 4. You may copy and distribute the Library (or a portion or
-derivative of it, under Section 2) in object code or executable form
-under the terms of Sections 1 and 2 above provided that you accompany
-it with the complete corresponding machine-readable source code, which
-must be distributed under the terms of Sections 1 and 2 above on a
-medium customarily used for software interchange.
-
- If distribution of object code is made by offering access to copy
-from a designated place, then offering equivalent access to copy the
-source code from the same place satisfies the requirement to
-distribute the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 5. A program that contains no derivative of any portion of the
-Library, but is designed to work with the Library by being compiled or
-linked with it, is called a "work that uses the Library". Such a
-work, in isolation, is not a derivative work of the Library, and
-therefore falls outside the scope of this License.
-
- However, linking a "work that uses the Library" with the Library
-creates an executable that is a derivative of the Library (because it
-contains portions of the Library), rather than a "work that uses the
-library". The executable is therefore covered by this License.
-Section 6 states terms for distribution of such executables.
-
- When a "work that uses the Library" uses material from a header file
-that is part of the Library, the object code for the work may be a
-derivative work of the Library even though the source code is not.
-Whether this is true is especially significant if the work can be
-linked without the Library, or if the work is itself a library. The
-threshold for this to be true is not precisely defined by law.
-
- If such an object file uses only numerical parameters, data
-structure layouts and accessors, and small macros and small inline
-functions (ten lines or less in length), then the use of the object
-file is unrestricted, regardless of whether it is legally a derivative
-work. (Executables containing this object code plus portions of the
-Library will still fall under Section 6.)
-
- Otherwise, if the work is a derivative of the Library, you may
-distribute the object code for the work under the terms of Section 6.
-Any executables containing that work also fall under Section 6,
-whether or not they are linked directly with the Library itself.
-
- 6. As an exception to the Sections above, you may also combine or
-link a "work that uses the Library" with the Library to produce a
-work containing portions of the Library, and distribute that work
-under terms of your choice, provided that the terms permit
-modification of the work for the customer's own use and reverse
-engineering for debugging such modifications.
-
- You must give prominent notice with each copy of the work that the
-Library is used in it and that the Library and its use are covered by
-this License. You must supply a copy of this License. If the work
-during execution displays copyright notices, you must include the
-copyright notice for the Library among them, as well as a reference
-directing the user to the copy of this License. Also, you must do one
-of these things:
-
- a) Accompany the work with the complete corresponding
- machine-readable source code for the Library including whatever
- changes were used in the work (which must be distributed under
- Sections 1 and 2 above); and, if the work is an executable linked
- with the Library, with the complete machine-readable "work that
- uses the Library", as object code and/or source code, so that the
- user can modify the Library and then relink to produce a modified
- executable containing the modified Library. (It is understood
- that the user who changes the contents of definitions files in the
- Library will not necessarily be able to recompile the application
- to use the modified definitions.)
-
- b) Use a suitable shared library mechanism for linking with the
- Library. A suitable mechanism is one that (1) uses at run time a
- copy of the library already present on the user's computer system,
- rather than copying library functions into the executable, and (2)
- will operate properly with a modified version of the library, if
- the user installs one, as long as the modified version is
- interface-compatible with the version that the work was made with.
-
- c) Accompany the work with a written offer, valid for at
- least three years, to give the same user the materials
- specified in Subsection 6a, above, for a charge no more
- than the cost of performing this distribution.
-
- d) If distribution of the work is made by offering access to copy
- from a designated place, offer equivalent access to copy the above
- specified materials from the same place.
-
- e) Verify that the user has already received a copy of these
- materials or that you have already sent this user a copy.
-
- For an executable, the required form of the "work that uses the
-Library" must include any data and utility programs needed for
-reproducing the executable from it. However, as a special exception,
-the materials to be distributed need not include anything that is
-normally distributed (in either source or binary form) with the major
-components (compiler, kernel, and so on) of the operating system on
-which the executable runs, unless that component itself accompanies
-the executable.
-
- It may happen that this requirement contradicts the license
-restrictions of other proprietary libraries that do not normally
-accompany the operating system. Such a contradiction means you cannot
-use both them and the Library together in an executable that you
-distribute.
-
- 7. You may place library facilities that are a work based on the
-Library side-by-side in a single library together with other library
-facilities not covered by this License, and distribute such a combined
-library, provided that the separate distribution of the work based on
-the Library and of the other library facilities is otherwise
-permitted, and provided that you do these two things:
-
- a) Accompany the combined library with a copy of the same work
- based on the Library, uncombined with any other library
- facilities. This must be distributed under the terms of the
- Sections above.
-
- b) Give prominent notice with the combined library of the fact
- that part of it is a work based on the Library, and explaining
- where to find the accompanying uncombined form of the same work.
-
- 8. You may not copy, modify, sublicense, link with, or distribute
-the Library except as expressly provided under this License. Any
-attempt otherwise to copy, modify, sublicense, link with, or
-distribute the Library is void, and will automatically terminate your
-rights under this License. However, parties who have received copies,
-or rights, from you under this License will not have their licenses
-terminated so long as such parties remain in full compliance.
-
- 9. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Library or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Library (or any work based on the
-Library), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Library or works based on it.
-
- 10. Each time you redistribute the Library (or any work based on the
-Library), the recipient automatically receives a license from the
-original licensor to copy, distribute, link with or modify the Library
-subject to these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties with
-this License.
-
- 11. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Library at all. For example, if a patent
-license would not permit royalty-free redistribution of the Library by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any
-particular circumstance, the balance of the section is intended to apply,
-and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 12. If the distribution and/or use of the Library is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Library under this License may add
-an explicit geographical distribution limitation excluding those countries,
-so that distribution is permitted only in or among countries not thus
-excluded. In such case, this License incorporates the limitation as if
-written in the body of this License.
-
- 13. The Free Software Foundation may publish revised and/or new
-versions of the Lesser General Public License from time to time.
-Such new versions will be similar in spirit to the present version,
-but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Library
-specifies a version number of this License which applies to it and
-"any later version", you have the option of following the terms and
-conditions either of that version or of any later version published by
-the Free Software Foundation. If the Library does not specify a
-license version number, you may choose any version ever published by
-the Free Software Foundation.
-
- 14. If you wish to incorporate parts of the Library into other free
-programs whose distribution conditions are incompatible with these,
-write to the author to ask for permission. For software which is
-copyrighted by the Free Software Foundation, write to the Free
-Software Foundation; we sometimes make exceptions for this. Our
-decision will be guided by the two goals of preserving the free status
-of all derivatives of our free software and of promoting the sharing
-and reuse of software generally.
-
- NO WARRANTY
-
- 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
-WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
-EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
-OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
-KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
-LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
-THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
-WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
-AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
-FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
-CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
-LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
-RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
-FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
-SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGES.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Libraries
-
- If you develop a new library, and you want it to be of the greatest
-possible use to the public, we recommend making it free software that
-everyone can redistribute and change. You can do so by permitting
-redistribution under these terms (or, alternatively, under the terms of the
-ordinary General Public License).
-
- To apply these terms, attach the following notices to the library. It is
-safest to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least the
-"copyright" line and a pointer to where the full notice is found.
-
- <one line to give the library's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This library is free software; you can redistribute it and/or
- modify it under the terms of the GNU Lesser General Public
- License as published by the Free Software Foundation; either
- version 2.1 of the License, or (at your option) any later version.
-
- This library is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public
- License along with this library; if not, write to the Free Software
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the library, if
-necessary. Here is a sample; alter the names:
-
- Yoyodyne, Inc., hereby disclaims all copyright interest in the
- library `Frob' (a library for tweaking knobs) written by James Random Hacker.
-
- <signature of Ty Coon>, 1 April 1990
- Ty Coon, President of Vice
-
-That's all there is to it!
-
diff --git a/LICENCE_WTFPL b/LICENCE_WTFPL
deleted file mode 100644
index 652d3783..00000000
--- a/LICENCE_WTFPL
+++ /dev/null
@@ -1,13 +0,0 @@
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- Version 2, December 2004
-
- Copyright (C) 2014 Rene Treffer <treffer+wtfpl@measite.de>
-
- Everyone is permitted to copy and distribute verbatim or modified
- copies of this license document, and changing it is allowed as long
- as the name is changed.
-
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. You just DO WHAT THE FUCK YOU WANT TO.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/README.md b/README.md
index 3c1417a1..c28d7e42 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,297 @@
-MiniDNS
--------
+# Conversations
-MiniDNS is a minimal dns client library for android. It can parse a basic set
-of resource records (A, AAAA, NS, SRV) and is easy to use and extend.
+Conversations: the very last word in instant messaging
-This library is not intended to be used as a DNS server. You might want to
-look into dnsjava for such functionality.
+[![Google Play](http://developer.android.com/images/brand/en_generic_rgb_wo_45.png)](https://play.google.com/store/apps/details?id=eu.siacs.conversations)
+
+![screenshots](https://raw.githubusercontent.com/siacs/Conversations/master/screenshots.png)
+
+## Design principles
+
+* Be as beautiful and easy to use as possible without sacrificing security or
+ privacy
+* Rely on existing, well established protocols (XMPP)
+* Do not require a Google Account or specifically Google Cloud Messaging (GCM)
+* Require as few permissions as possible
+
+## Features
+
+* End-to-end encryption with either [OTR](https://otr.cypherpunks.ca/) or [OpenPGP](http://www.openpgp.org/about_openpgp/)
+* Sending and receiving images
+* Indication when your contact has read your message
+* Intuitive UI that follows Android Design guidelines
+* Pictures / Avatars for your Contacts
+* Syncs with desktop client
+* Conferences (with support for bookmarks)
+* Address book integration
+* Multiple accounts / unified inbox
+* Very low impact on battery life
+
+
+### XMPP Features
+
+Conversations works with every XMPP server out there. However XMPP is an
+extensible protocol. These extensions are standardized as well in so called
+XEP's. Conversations supports a couple of these to make the overall user
+experience better. There is a chance that your current XMPP server does not
+support these extensions; therefore to get the most out of Conversations you
+should consider either switching to an XMPP server that does or — even better —
+run your own XMPP server for you and your friends. These XEP's are:
+
+* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Will be used to transfer
+ files if both parties are behind a firewall (NAT).
+* XEP-0138: Stream Compression saves bandwidth
+* XEP-0163: Personal Eventing Protocol for avatars
+* XEP-0198: Stream Management allows XMPP to survive small network outages and
+ changes of the underlying TCP connection.
+* XEP-0280: Message Carbons which automatically syncs the messages you send to
+ your desktop client and thus allows you to switch seamlessly from your mobile
+ client to your desktop client and back within one conversation.
+* XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections
+* XEP-0352: Client State Indication let the server know whether or not
+ Conversations is in the background. Allows the server to save bandwidth by
+ withholding unimportant packages.
+
+## Team
+
+#### Head of Development
+
+* [Daniel Gultsch](https://github.com/inputmice)
+
+#### Code Contributions
+
+(In order of appearance)
+
+* [Rene Treffer](https://github.com/rtreffer)
+* [Andreas Straub](https://github.com/strb)
+* [Alethea Butler](https://github.com/alethea)
+* [M. Dietrich](https://github.com/emdete)
+* [betheg](https://github.com/betheg)
+
+#### Logo
+
+* [Diego Turtulici](http://efesto.eigenlab.org/~diesys)
+
+#### Translations
+
+* [Sergio Cárdenas](https://github.com/kruks23) (Spanish)
+* [Benoit Bouvarel](https://github.com/BenoitBouvarel) (French)
+* [Daniel Gultsch](https://github.com/iNPUTmice) (German)
+* [Aitor Beriain](https://github.com/beriain) (Basque)
+* [Ilia Rostovtsev](https://github.com/qooob) (Russian)
+* [Jelmer Vernooij](https://github.com/jelmer) (Dutch)
+* [Anders Sandblad](https://github.com/andersruneson) (Swedish)
+* [Aizaz AZ](http://www.linkedin.com/in/aizazhaider) (Chinese)
+
+## FAQ
+
+### General
+
+#### How do I install Conversations?
+
+Conversations is entirely open source and licensed under GPLv3. So if you are a
+software developer you can check out the sources from GitHub and use ant to
+build your apk file.
+
+The more convenient way — which not only gives you automatic updates but also
+supports the further development of Conversations - is to buy the App in the
+Google [Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations).
+
+#### I don't have a Google Account but I would still like to make a contribution
+
+I accept donations over PayPal, Bitcoin and Flattr. For donations via PayPal you
+can use the email address `donate@siacs.eu` or the button below.
+
+[![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CW3SYT3KG5PDL)
+
+**Disclaimer:** I'm not a huge fan of PayPal and their business policies. For
+larger contributions please get in touch with me beforehand and we can talk
+about bank transfer (SEPA).
+
+My Bitcoin Address is: `1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu`
+
+
+[![Flattr this!](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=inputmice&url=http%3A%2F%2Fconversations.siacs.eu&title=Conversations&tags=github&category=software)
+
+#### How do I create an account?
+
+XMPP, like email, is a federated protocol which means that there is not one
+company you can create an 'official XMPP account' with. Instead there are
+hundreds, or even thousands, of provider out there. To find one use a web search
+engine of your choice. Or maybe your university has one. Or you can run your
+own. Or ask a friend to run one. Once you've found one, you can use
+Conversations to create an account. Just select 'register new account on server'
+within the create account dialog.
+
+#### Conversations doesn't work for me. Where can I get help?
+
+You can join our conference room on `conversations@conference.siacs.eu` A lot of
+people in there are able to answer basic questions about the usage of
+Conversations or can provide you with tips on running your own XMPP server. If
+you found a bug or your app crashes please read the Developer / Report Bugs
+section of this document.
+
+#### I need professional support with Conversations or setting up my server
+
+I'm available for hire. Contact me at `inputmice@siacs.eu`.
+
+#### How does the address book integration work?
+
+The address book integration was designed to protect your privacy. Conversations
+neither uploads contacts from your address book to your server nor fills your
+address book with unnecessary contacts from your online roster. If you manually
+add a Jabber ID to your phones address book Conversations will use the name and
+the profile picture of this contact. To make the process of adding Jabber IDs to
+your address book easier you can click on the profile picture in the contact
+details within Conversations. This will start an "add to address book" intent
+with the JID as the payload. This doesn't require Conversations to have write
+permissions on your address book but also doesn't require you to copy/paste a
+JID from one app to another.
+
+#### I get 'delivery failed' on my messages
+
+If you get delivery failed on images it's probably because the recipient lost
+network connectivity during reception. In that case you can try it again at a
+later time.
+
+For text messages the answer to your question is a little bit more complex.
+When you see 'delivery failed' on text messages, it is always something that is
+being reported by the server. The most common reason for this is that the
+recipient failed to resume a connection. When a client loses connectivity for a
+short time the client usually has a five minute window to pick up that
+connection again. When the client fails to do so because the network
+connectivity is out for longer than that all messages sent to that client will
+be returned to the sender resulting in a delivery failed.
+
+Other less common reasons are that the message you sent didn't meet some
+criteria enforced by the server (too large, too many). Another reason could be
+that the recipient is offline and the server doesn't provide offline storage.
+
+Usually you are able to distinguish between these two groups in the fact that
+the first one happens always after some time and the second one happens almost
+instantly.
+
+#### Where can I see the status of my contacts? How can I set a status or priority?
+
+Statuses are a horrible metric. Setting them manually to a proper value rarely
+works because users are either lazy or just forget about them. Setting them
+automatically does not provide quality results either. Keyboard or mouse
+activity as indicator for example fails when the user is just looking at
+something (reading an article, watching a movie). Furthermore automatic setting
+of status always implies an impact on your privacy (are you sure you want
+everybody in your contact list to know that you have been using your computer at
+4am‽).
+
+In the past status has been used to judge the likelihood of whether or not your
+messages are being read. This is no longer necessary. With Chat Markers
+(XEP-0333, supported by Conversations since 0.4) we have the ability to **know**
+whether or not your messages are being read. Similar things can be said for
+priorities. In the past priorities have been used (by servers, not by clients!)
+to route your messages to one specific client. With carbon messages (XEP-0280,
+supported by Conversations since 0.1) this is no longer necessary. Using
+priorities to route OTR messages isn't practical either because they are not
+changeable on the fly. Metrics like last active client (the client which sent
+the last message) are much better.
+
+Unfortunately these modern replacements for legacy XMPP features are not widely
+adopted. However Conversations should be an instant messenger for the future and
+instead of making Conversations compatible with the past we should work on
+implementing new, improved technologies and getting them into other XMPP clients
+as well.
+
+Making these status and priority optional isn't a solution either because
+Conversations is trying to get rid of old behaviours and set an example for
+other clients.
+
+#### Conversations is missing a certain feature
+
+I'm open for new feature suggestions. You can use the [issue tracker][issues] on
+GitHub. Please take some time to browse through the issues to see if someone
+else already suggested it. Be assured that I read each and every ticket. If I
+like it I will leave it open until it's implemented. If I don't like it I will
+close it (usually with a short comment). If I don't comment on an feature
+request that's probably a good sign because this means I agree with you.
+Commenting with +1 on either open or closed issues won't change my mind, nor
+will it accelerate the development.
+
+#### You closed my feature request but I want it really really badly
+
+Just write it yourself and send me a pull request. If I like it I will happily
+merge it if I don't at least you and like minded people get to enjoy it.
+
+#### I need a feature and I need it now!
+
+I am available for hire. Contact me via XMPP: `inputmice@siacs.eu`
+
+### Security
+
+#### Why are there two end-to-end encryption methods and which one should I choose?
+
+In most cases OTR should be the encryption method of choice. It works out of the
+box with most contacts as long as they are online. However PGP can, in some
+cases, (message carbons to multiple clients) be more flexible.
+
+#### How do I use OpenPGP
+
+Before you continue reading you should note that the OpenPGP support in
+Conversations is experimental. This is not because it will make the app unstable
+but because the fundamental concepts of PGP aren't ready for widespread use.
+The way PGP works is that you trust Key IDs instead of JID's or email addresses.
+So in theory your contact list should consist of Public-Key-IDs instead of
+JID's. But of course no email or XMPP client out there implements these
+concepts. Plus PGP in the context of instant messaging has a couple of
+downsides: It is vulnerable to replay attacks, it is rather verbose, and
+decrypting and encrypting takes longer than OTR. It is however asynchronous and
+works well with message carbons.
+
+To use OpenPGP you have to install the open source app
+[OpenKeychain](www.openkeychain.org) and then long press on the account in
+manage accounts and choose renew PGP announcement from the contextual menu.
+
+#### How does the encryption for conferences work?
+
+For conferences the only supported encryption method is OpenPGP (OTR does not
+work with multiple participants). Every participant has to announce their
+OpenPGP key (see answer above). If you would like to send encrypted messages to
+a conference you have to make sure that you have every participant's public key
+in your OpenKeychain. Right now there is no check in Conversations to ensure
+that. You have to take care of that yourself. Go to the conference details and
+touch every key id (The hexadecimal number below a contact). This will send you
+to OpenKeychain which will assist you on adding the key. This works best in
+very small conferences with contacts you are already using OpenPGP with. This
+feature is regarded experimental. Conversations is the only client that uses
+XEP-0027 with conferences. (The XEP neither specifically allows nor disallows
+this.)
+
+### Development
+
+#### How do I build Conversations
+
+Make sure to have ANDROID_HOME point to your Android SDK
+
+ git clone https://github.com/siacs/Conversations.git
+ cd Conversations
+ git submodule update --init --recursive
+ ant clean
+ ant debug
+
+#### How do I debug Conversations
+
+If something goes wrong Conversations usually exposes very little information in
+the UI (other than the fact that something didn't work). However with adb
+(android debug bridge) you squeeze some more information out of Conversations.
+These information are especially useful if you are experiencing trouble with
+your connection or with file transfer.
+
+ adb -d logcat -v time -s conversations
+
+#### I found a bug
+
+Please report it to our [issue tracker][issues]. If your app crashes please
+provide a stack trace. If you are experiencing misbehaviour please provide
+detailed steps to reproduce. Always mention whether you are running the latest
+Play Store version or the current HEAD. If you are having problems connecting to
+your XMPP server your file transfer doesn’t work as expected please always
+include a logcat debug output with your issue (see above).
+
+[issues]: https://github.com/siacs/Conversations/issues
diff --git a/art/LICENSE b/art/LICENSE
new file mode 100644
index 00000000..34ec65f3
--- /dev/null
+++ b/art/LICENSE
@@ -0,0 +1,425 @@
+Attribution-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More_considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. BY-SA Compatible License means a license listed at
+ creativecommons.org/compatiblelicenses, approved by Creative
+ Commons as essentially the equivalent of this Public License.
+
+ d. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ e. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ g. License Elements means the license attributes listed in the name
+ of a Creative Commons Public License. The License Elements of this
+ Public License are Attribution and ShareAlike.
+
+ h. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ i. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ j. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ k. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ l. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ m. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part; and
+
+ b. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. Additional offer from the Licensor -- Adapted Material.
+ Every recipient of Adapted Material from You
+ automatically receives an offer from the Licensor to
+ exercise the Licensed Rights in the Adapted Material
+ under the conditions of the Adapter's License You apply.
+
+ c. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ b. ShareAlike.
+
+ In addition to the conditions in Section 3(a), if You Share
+ Adapted Material You produce, the following conditions also apply.
+
+ 1. The Adapter's License You apply must be a Creative Commons
+ license with the same License Elements, this version or
+ later, or a BY-SA Compatible License.
+
+ 2. You must include the text of, or the URI or hyperlink to, the
+ Adapter's License You apply. You may satisfy this condition
+ in any reasonable manner based on the medium, means, and
+ context in which You Share Adapted Material.
+
+ 3. You may not offer or impose any additional or different terms
+ or conditions on, or apply any Effective Technological
+ Measures to, Adapted Material that restrict exercise of the
+ rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material,
+
+ including for purposes of Section 3(b); and
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public licenses.
+Notwithstanding, Creative Commons may elect to apply one of its public
+licenses to material it publishes and in those instances will be
+considered the "Licensor." Except for the limited purpose of indicating
+that material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the public
+licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/art/conversations.svg b/art/conversations.svg
new file mode 100644
index 00000000..621b4124
--- /dev/null
+++ b/art/conversations.svg
@@ -0,0 +1,381 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="512"
+ height="512"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="conversation.svg"
+ inkscape:export-filename="/home/diesys/diesys/grafica/conversation/2/conversation.png"
+ inkscape:export-xdpi="100"
+ inkscape:export-ydpi="100">
+ <defs
+ id="defs4">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3913">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3915" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop3917" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3818">
+ <stop
+ style="stop-color:#669900;stop-opacity:1"
+ offset="0"
+ id="stop3820" />
+ <stop
+ style="stop-color:#99cc00;stop-opacity:1"
+ offset="1"
+ id="stop3822" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3824"
+ cx="212.07048"
+ cy="1045.9178"
+ fx="212.07048"
+ fy="1045.9178"
+ r="238.57143"
+ gradientTransform="matrix(1.9491621,-0.90817722,0.65829208,1.4128498,-879.63121,-248.98648)"
+ gradientUnits="userSpaceOnUse" />
+ <filter
+ inkscape:collect="always"
+ id="filter3836">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.3841605"
+ id="feGaussianBlur3838" />
+ </filter>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3913"
+ id="radialGradient3919"
+ cx="362.98563"
+ cy="379.77524"
+ fx="362.98563"
+ fy="379.77524"
+ r="139.95312"
+ gradientTransform="matrix(1.3800477,1.0445431,-1.3325077,1.7605059,339.09383,-577.83938)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="-155.75885"
+ x2="114.59022"
+ y1="35.545681"
+ x1="114.55434"
+ id="linearGradient3794"
+ xlink:href="#linearGradient3788"
+ inkscape:collect="always" />
+ <linearGradient
+ id="linearGradient3788">
+ <stop
+ id="stop3790"
+ offset="0"
+ style="stop-color:#1eed00;stop-opacity:1;" />
+ <stop
+ id="stop3792"
+ offset="1"
+ style="stop-color:#abff28;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3821">
+ <stop
+ style="stop-color:#ff283d;stop-opacity:1;"
+ offset="0"
+ id="stop3823" />
+ <stop
+ style="stop-color:#ff28ae;stop-opacity:1;"
+ offset="1"
+ id="stop3825" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4543">
+ <stop
+ style="stop-color:#2e45bf;stop-opacity:1;"
+ offset="0"
+ id="stop4545" />
+ <stop
+ style="stop-color:#28a7ff;stop-opacity:1;"
+ offset="1"
+ id="stop4547" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4098"
+ id="radialGradient4106"
+ cx="141.85023"
+ cy="147.36685"
+ fx="141.85023"
+ fy="147.36685"
+ r="172.26643"
+ gradientTransform="matrix(0.43684283,1.3119293,-2.2907273,0.76276042,502.45961,-107.61591)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4098">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop4100" />
+ <stop
+ style="stop-color:#e6e6e6;stop-opacity:1"
+ offset="1"
+ id="stop4102" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4098"
+ id="linearGradient3833"
+ x1="273.81851"
+ y1="764.74677"
+ x2="304.14023"
+ y2="936.47272"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4098"
+ id="linearGradient3853"
+ gradientUnits="userSpaceOnUse"
+ x1="273.81851"
+ y1="764.74677"
+ x2="304.14023"
+ y2="936.47272" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.24748737"
+ inkscape:cx="116.21963"
+ inkscape:cy="99.822919"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1280"
+ inkscape:window-height="754"
+ inkscape:window-x="0"
+ inkscape:window-y="23"
+ inkscape:window-maximized="1"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-to-guides="false"
+ inkscape:snap-grids="false"
+ inkscape:object-paths="true"
+ inkscape:object-nodes="false"
+ inkscape:snap-nodes="false">
+ <sodipodi:guide
+ orientation="1,0"
+ position="0,534.28571"
+ id="guide3004" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="394.28571,511.42857"
+ id="guide3006" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="511.42857,320"
+ id="guide3008" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="401.42857,0"
+ id="guide3010" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="17.142857,258.57143"
+ id="guide3012" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="327.14286,494.28571"
+ id="guide3014" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="324.28571,17.142857"
+ id="guide3016" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="494.28571,237.14286"
+ id="guide3018" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="255.71429,302.85714"
+ id="guide3022" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="495.71429,255.71429"
+ id="guide3024" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="660,-315"
+ id="guide3904" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="554.28571,475.71429"
+ id="guide3931" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="581.42857,244.28571"
+ id="guide3933" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-540.36218)"
+ style="display:inline">
+ <rect
+ ry="20.359909"
+ y="563.69794"
+ x="17.857141"
+ height="475.09274"
+ width="477.14285"
+ id="rect3826"
+ style="opacity:0.6;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;filter:url(#filter3836)" />
+ <rect
+ ry="15.742693"
+ y="558.07648"
+ x="17.142857"
+ height="48.838173"
+ width="477.14285"
+ id="rect3796"
+ style="fill:#c0ea44;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+ rx="15.714294" />
+ <rect
+ style="fill:url(#radialGradient3824);fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+ id="rect3026"
+ width="477.14285"
+ height="457.94995"
+ x="17.142857"
+ y="577.26935"
+ ry="20.359909" />
+ <path
+ sodipodi:nodetypes="ccsssscc"
+ inkscape:connector-curvature="0"
+ id="path3843"
+ d="M 440.75922,998.26318 414.19749,890.80009 c 16.8897,-27.15918 22.70855,-59.05025 22.70855,-93.16236 0,-99.1504 -82.19661,-179.50071 -183.57099,-179.50071 -101.37434,0 -183.570935,80.35031 -183.570935,179.50071 0,99.15039 82.196595,179.56005 183.570935,179.56005 33.65679,0 58.24146,-6.91097 89.32491,-18.47 z"
+ style="opacity:0.05000000000000000;fill:#000000;stroke:none;stroke-width:20;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:80, 80;stroke-dashoffset:0;stroke-linecap:butt;stroke-linejoin:round;fill-opacity:1" />
+ <path
+ style="fill:url(#radialGradient3919);fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;opacity:0.1"
+ d="M 413.71875 186.625 C 303.61523 186.625 214.375 274.21465 214.375 382.25 C 214.375 424.17883 227.82948 463.001 250.71875 494.84375 L 473.9375 494.84375 C 485.21689 494.84375 494.28125 485.77939 494.28125 474.5 L 494.28125 203.25 C 469.65076 192.56354 442.38179 186.625 413.71875 186.625 z "
+ id="rect3908"
+ transform="translate(0,540.36218)" />
+ <g
+ id="g3971"
+ transform="matrix(1.1625669,0,0,1.0778378,-139.43297,-63.26267)">
+ <g
+ id="g3945">
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.4;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="text3923">
+ <path
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3943"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="text3927">
+ <path
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;letter-spacing:-1.37633753px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3940"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ transform="translate(80,0)"
+ id="g3951">
+ <g
+ id="g3953"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.4;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path3955"
+ style="font-size:627.96289062px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889" />
+ </g>
+ <g
+ id="g3957"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path3959"
+ style="font-size:627.96289062px;font-weight:bold;letter-spacing:-1.37633753px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889" />
+ </g>
+ </g>
+ <g
+ id="g3961"
+ transform="translate(160,0)">
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.4;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="g3963">
+ <path
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3965"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="g3967">
+ <path
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;letter-spacing:-1.37633753px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3969"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ </g>
+ <path
+ sodipodi:nodetypes="ccsssscc"
+ inkscape:connector-curvature="0"
+ id="path3845"
+ d="M 444.75922,1002.2632 418.19749,894.80009 c 16.8897,-27.15918 22.70855,-59.05025 22.70855,-93.16236 0,-99.1504 -82.19661,-179.50071 -183.57099,-179.50071 -101.37434,0 -183.570935,80.35031 -183.570935,179.50071 0,99.15039 82.196595,179.56005 183.570935,179.56005 33.65679,0 58.24146,-6.91097 89.32491,-18.47 z"
+ style="opacity:0;fill:none;stroke:#000000;stroke-width:20;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:80, 80;stroke-dashoffset:0;fill-opacity:1" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3855"
+ d="m 253.3125,608.29968 c -4.57162,-0.002 -9.1209,0.0741 -10.84375,0.1875 -14.27382,0.9398 -25.20877,2.59141 -37.6875,5.75 -4.79596,1.21395 -8.46435,2.29027 -12.90625,3.75 -2.83656,0.93217 -7.14779,2.42208 -7.375,2.5625 -0.0697,0.0431 2.82515,7.91819 6.78125,18.5 0.0396,0.10605 0.14482,0.1174 0.28125,0.0625 0.9453,-0.38039 5.48292,-1.95002 7.59375,-2.625 18.16928,-5.80998 37.3261,-8.65347 56.3125,-8.375 2.67375,0.0392 6.0571,0.13508 7.5,0.21875 1.44289,0.0836 2.6507,0.13055 2.6875,0.0937 0.058,-0.058 1.3012,-19.70366 1.25,-19.75 -0.0105,-0.01 -1.19784,-0.0929 -2.65625,-0.1875 -1.7721,-0.11497 -6.36588,-0.18548 -10.9375,-0.1875 z m 94.09375,23.90625 c -0.086,0.0926 -9.46875,17.21685 -9.46875,17.28125 0,0.0158 0.43052,0.2686 0.96875,0.5625 9.87456,5.39206 21.05418,13.20405 29.84375,20.875 9.26433,8.08528 18.46008,18.02393 25.03125,27 0.55428,0.75718 0.67166,0.85859 0.875,0.71875 0.96294,-0.66221 15.622,-11.6872 15.625,-11.75 0.004,-0.0849 -2.83241,-3.84089 -4.03125,-5.34375 -4.7925,-6.00788 -8.99894,-10.68744 -14.65625,-16.34375 -9.04603,-9.04445 -17.54561,-16.09577 -28.03125,-23.25 -5.60544,-3.82453 -15.88873,-10.03821 -16.15625,-9.75 z m -233.3125,33.90625 -2.0625,2.1875 c -18.32375,19.26161 -32.32553,41.78507 -41.15625,66.25 -0.43225,1.19753 -0.81284,2.27121 -0.84375,2.40625 -0.0406,0.17743 2.51089,1.11193 9.15625,3.34375 5.05804,1.69873 9.27679,3.08775 9.375,3.09375 0.0982,0.006 0.77856,-1.65137 1.53125,-3.6875 7.72587,-20.89937 19.68924,-40.10992 35.375,-56.8125 l 2.59375,-2.78125 -6.96875,-7 -7,-7 z m 329.65625,97.75 -8.40625,1.625 c -4.62648,0.90809 -8.97544,1.75853 -9.6875,1.875 -0.88919,0.14544 -1.3125,0.29719 -1.3125,0.46875 0,0.13738 0.19807,1.38523 0.4375,2.78125 2.29501,13.3812 2.78822,29.39501 1.4375,46.90625 -0.61827,8.01542 -1.99398,17.86187 -3.3125,23.78125 -0.19825,0.89002 -0.32225,1.67775 -0.28125,1.71875 0.041,0.041 4.35259,0.98127 9.59375,2.09375 5.24116,1.11247 9.60165,2.01935 9.65625,2.03125 0.0546,0.0119 0.38836,-1.47098 0.75,-3.3125 2.85197,-14.52267 4.0984,-28.1704 4.125,-45.21875 0.0186,-11.95864 -0.67218,-20.67232 -2.4375,-31.375 l -0.5625,-3.375 z m -362.8125,54.375 -0.65625,0.0937 c -0.35822,0.052 -4.7537,0.57476 -9.75,1.15625 -4.9963,0.58149 -9.13608,1.10485 -9.1875,1.15625 -0.12057,0.12057 0.21425,2.4783 0.96875,7.0625 4.13481,25.12214 12.76972,47.74942 26.78125,70.125 0.38271,0.61117 0.79093,1.0844 0.90625,1.0625 0.30714,-0.0583 16.493,-10.358 16.5,-10.5 0.003,-0.0662 -0.67654,-1.19119 -1.5,-2.5 -3.62302,-5.75842 -8.43627,-14.81353 -11.09375,-20.875 -6.39324,-14.58238 -10.6048,-29.464 -12.78125,-45.2187 l -0.1875,-1.5625 z m 350.03125,100.1875 c -0.39823,0.008 -18.75391,4.56641 -18.875,4.6875 -0.0832,0.0832 18.69913,76.44787 19,77.25002 0.0482,0.1284 0.17116,0.1978 0.28125,0.1562 0.1101,-0.042 4.39957,-1.10959 9.53125,-2.37497 5.13167,-1.26539 9.33975,-2.4018 9.34375,-2.5 0.004,-0.0982 -4.26803,-17.49777 -9.5,-38.6875 -7.47046,-30.25568 -9.5684,-38.53525 -9.78125,-38.53125 z m -270.4375,22.6875 c -0.34766,0.0112 -1.03246,1.12684 -5.34375,8.1875 -2.81434,4.60909 -5.125,8.42085 -5.125,8.46875 0,0.11606 2.91136,1.85904 6,3.625 18.97988,10.85192 39.98244,18.52086 61.5,22.4375 3.0488,0.55494 4.81689,0.85401 9.375,1.53125 0.58929,0.0876 1.16293,0.1756 1.28125,0.1875 0.26735,0.0268 0.20708,0.53299 1.59375,-9.71875 0.63767,-4.71429 1.193,-8.85633 1.25,-9.21875 0.10005,-0.63553 0.0731,-0.6593 -0.5,-0.75 -0.32684,-0.0517 -2.28125,-0.35029 -4.34375,-0.65625 -18.93976,-2.80953 -38.43135,-9.25433 -55.46875,-18.3125 -3.01785,-1.60448 -8.63366,-4.78954 -10.0625,-5.71875 -0.045,-0.0293 -0.10658,-0.0641 -0.15625,-0.0625 z m 181.90625,7.71875 c -1.68213,0.11043 -3.90782,0.86376 -8.875,2.65625 -9.59501,3.46252 -20.49118,7.0151 -25.40625,8.28125 -0.71107,0.18318 -1.3115,0.43024 -1.3125,0.53125 -0.002,0.18251 4.8611,18.86584 4.9375,18.96875 0.047,0.0633 5.38958,-1.44515 8.71875,-2.46875 4.32782,-1.33065 11.61031,-3.77066 16.8125,-5.625 l 5.15625,-1.84375 18.9375,7.625 c 15.40636,6.21018 18.93675,7.59721 19.03125,7.375 0.76943,-1.80995 7.31715,-18.1024 7.28125,-18.125 -0.32947,-0.20787 -41.72627,-16.78524 -42.375,-16.96875 -1.06807,-0.30214 -1.89697,-0.47251 -2.90625,-0.40625 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;opacity:0.50000000000000000;stroke-opacity:1;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none" />
+ </g>
+</svg>
diff --git a/art/conversations_baloon.svg b/art/conversations_baloon.svg
new file mode 100644
index 00000000..5a993cce
--- /dev/null
+++ b/art/conversations_baloon.svg
@@ -0,0 +1,422 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="512"
+ height="512"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="conversation_bubble.svg"
+ inkscape:export-filename="/home/diesys/diesys/grafica/conversation/conversation_bubble.png"
+ inkscape:export-xdpi="100"
+ inkscape:export-ydpi="100">
+ <defs
+ id="defs4">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3913">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3915" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop3917" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3818">
+ <stop
+ style="stop-color:#669900;stop-opacity:1"
+ offset="0"
+ id="stop3820" />
+ <stop
+ style="stop-color:#99cc00;stop-opacity:1"
+ offset="1"
+ id="stop3822" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3824"
+ cx="212.07048"
+ cy="1045.9178"
+ fx="212.07048"
+ fy="1045.9178"
+ r="238.57143"
+ gradientTransform="matrix(1.9491621,-0.90817722,0.65829208,1.4128498,-879.63121,-248.98648)"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3913"
+ id="radialGradient3919"
+ cx="362.98563"
+ cy="379.77524"
+ fx="362.98563"
+ fy="379.77524"
+ r="139.95312"
+ gradientTransform="matrix(1.3800477,1.0445431,-1.3325077,1.7605059,339.09383,-577.83938)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="-155.75885"
+ x2="114.59022"
+ y1="35.545681"
+ x1="114.55434"
+ id="linearGradient3794"
+ xlink:href="#linearGradient3788"
+ inkscape:collect="always" />
+ <linearGradient
+ id="linearGradient3788">
+ <stop
+ id="stop3790"
+ offset="0"
+ style="stop-color:#1eed00;stop-opacity:1;" />
+ <stop
+ id="stop3792"
+ offset="1"
+ style="stop-color:#abff28;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3821">
+ <stop
+ style="stop-color:#ff283d;stop-opacity:1;"
+ offset="0"
+ id="stop3823" />
+ <stop
+ style="stop-color:#ff28ae;stop-opacity:1;"
+ offset="1"
+ id="stop3825" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4543">
+ <stop
+ style="stop-color:#2e45bf;stop-opacity:1;"
+ offset="0"
+ id="stop4545" />
+ <stop
+ style="stop-color:#28a7ff;stop-opacity:1;"
+ offset="1"
+ id="stop4547" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4098">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop4100" />
+ <stop
+ style="stop-color:#e6e6e6;stop-opacity:1"
+ offset="1"
+ id="stop4102" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4098"
+ id="linearGradient3833"
+ x1="273.81851"
+ y1="764.74677"
+ x2="304.14023"
+ y2="936.47272"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4098"
+ id="linearGradient3853"
+ gradientUnits="userSpaceOnUse"
+ x1="273.81851"
+ y1="764.74677"
+ x2="304.14023"
+ y2="936.47272" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3863"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,170.11831)"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3866"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,170.11831)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3870"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,170.11831)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3913"
+ id="radialGradient3873"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.3800477,1.0445431,-1.3325077,1.7605059,339.09383,-577.83938)"
+ cx="321.75275"
+ cy="386.38751"
+ fx="321.75275"
+ fy="386.38751"
+ r="139.95312" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3880"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,-370.24387)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3913"
+ id="radialGradient3883"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4430075,-0.63865195,0.50745433,1.1475866,-594.40824,44.803037)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ <filter
+ inkscape:collect="always"
+ id="filter3895">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.0013623"
+ id="feGaussianBlur3897" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.24748737"
+ inkscape:cx="222.83124"
+ inkscape:cy="467.98135"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1280"
+ inkscape:window-height="754"
+ inkscape:window-x="0"
+ inkscape:window-y="23"
+ inkscape:window-maximized="1"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-to-guides="true"
+ inkscape:snap-grids="false"
+ inkscape:object-paths="true"
+ inkscape:object-nodes="false"
+ inkscape:snap-nodes="false">
+ <sodipodi:guide
+ orientation="1,0"
+ position="0,534.28571"
+ id="guide3004" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="394.28571,511.42857"
+ id="guide3006" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="511.42857,320"
+ id="guide3008" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="401.42857,0"
+ id="guide3010" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="17.142857,258.57143"
+ id="guide3012" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="327.14286,494.28571"
+ id="guide3014" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="324.28571,17.142857"
+ id="guide3016" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="494.28571,237.14286"
+ id="guide3018" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="255.71429,302.85714"
+ id="guide3022" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="495.71429,255.71429"
+ id="guide3024" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="660,-315"
+ id="guide3904" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="554.28571,475.71429"
+ id="guide3931" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="581.42857,244.28571"
+ id="guide3933" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-540.36218)"
+ style="display:inline">
+ <path
+ d="m 253.34375,605.78125 c -107.90463,0 -195.9375,85.86121 -195.9375,191.84375 0,105.98253 88.02779,191.90625 195.9375,191.90625 33.55862,0 59.4324,-6.89467 88.96875,-17.625 l 93.8125,37.81255 A 12.359798,12.359798 0 0 0 452.75,995.28125 L 427.34375,892.59375 C 443.67389,863.93074 449.25,831.2919 449.25,797.625 449.25,691.64506 361.24842,605.78125 253.34375,605.78125 z"
+ id="path3885"
+ style="opacity:0.6;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter3895)"
+ inkscape:original="M 253.34375 618.125 C 151.96941 618.125 69.75 698.4746 69.75 797.625 C 69.75 896.77539 151.96941 977.1875 253.34375 977.1875 C 287.00054 977.1875 311.5728 970.27778 342.65625 958.71875 L 440.75 998.25 L 414.1875 890.8125 C 431.0772 863.65332 436.90625 831.73711 436.90625 797.625 C 436.90625 698.4746 354.71813 618.125 253.34375 618.125 z "
+ inkscape:radius="12.358562"
+ sodipodi:type="inkscape:offset"
+ transform="matrix(1.1776575,0,0,1.1781783,-45.132882,-150.91395)" />
+ <path
+ sodipodi:type="inkscape:offset"
+ inkscape:radius="12.358562"
+ inkscape:original="M 253.34375 618.125 C 151.96941 618.125 69.75 698.4746 69.75 797.625 C 69.75 896.77539 151.96941 977.1875 253.34375 977.1875 C 287.00054 977.1875 311.5728 970.27778 342.65625 958.71875 L 440.75 998.25 L 414.1875 890.8125 C 431.0772 863.65332 436.90625 831.73711 436.90625 797.625 C 436.90625 698.4746 354.71813 618.125 253.34375 618.125 z "
+ style="fill:url(#radialGradient3870);fill-opacity:1;stroke:none"
+ id="path3868"
+ d="m 253.34375,605.78125 c -107.90463,0 -195.9375,85.86121 -195.9375,191.84375 0,105.98253 88.02779,191.90625 195.9375,191.90625 33.55862,0 59.4324,-6.89467 88.96875,-17.625 l 93.8125,37.81255 A 12.359798,12.359798 0 0 0 452.75,995.28125 L 427.34375,892.59375 C 443.67389,863.93074 449.25,831.2919 449.25,797.625 449.25,691.64506 361.24842,605.78125 253.34375,605.78125 z"
+ transform="matrix(1.1776575,0,0,1.1781783,-45.132882,-155.6267)" />
+ <path
+ style="opacity:0.19211821;fill:url(#radialGradient3883);fill-opacity:1;stroke:none"
+ d="m 442.08605,700.89397 c -129.66422,0 -234.75863,103.19621 -234.75863,230.48113 0,26.84957 4.6841,52.62718 13.28548,76.5811 10.65333,1.4828 21.54531,2.2461 32.60637,2.2461 39.52053,0 69.99101,-8.1231 104.7747,-20.7651 l 110.479,44.5494 a 14.555607,14.562048 0 0 0 19.57853,-17.0097 L 458.13167,895.99293 c 19.23127,-33.77016 25.79804,-72.22452 25.79804,-111.89014 0,-28.84573 -5.53074,-56.41202 -15.60395,-81.77294 -8.61503,-0.94041 -17.37147,-1.43588 -26.23971,-1.43588 z"
+ id="path3878"
+ inkscape:connector-curvature="0" />
+ <g
+ id="g3971"
+ transform="matrix(1.3691054,0,0,1.2698854,-209.33716,-230.16141)">
+ <g
+ id="g3945">
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.4;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="text3923">
+ <path
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3943"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="text3927">
+ <path
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;letter-spacing:-1.37633753px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3940"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ transform="translate(80,0)"
+ id="g3951">
+ <g
+ id="g3953"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.4;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path3955"
+ style="font-size:627.96289062px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889" />
+ </g>
+ <g
+ id="g3957"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path3959"
+ style="font-size:627.96289062px;font-weight:bold;letter-spacing:-1.37633753px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889" />
+ </g>
+ </g>
+ <g
+ id="g3961"
+ transform="translate(160,0)">
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.4;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="g3963">
+ <path
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3965"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="g3967">
+ <path
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889"
+ style="font-size:627.96289062px;font-weight:bold;letter-spacing:-1.37633753px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3969"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ </g>
+ <path
+ sodipodi:nodetypes="ccsssscc"
+ inkscape:connector-curvature="0"
+ id="path3845"
+ d="M 478.64112,1025.218 447.36049,898.60749 c 19.89028,-31.99834 26.74288,-69.57172 26.74288,-109.76189 0,-116.81686 -96.79943,-211.48385 -216.18374,-211.48385 -119.38425,0 -216.183656,94.66699 -216.183656,211.48385 0,116.81685 96.799406,211.5536 216.183656,211.5536 39.63617,0 68.58847,-8.14219 105.19417,-21.76075 z"
+ style="opacity:0;fill:none;stroke:#000000;stroke-width:23.55835724;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:94.23343197, 94.23343197;stroke-dashoffset:0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3855"
+ d="m 253.18246,561.05889 c -5.38379,-0.002 -10.7413,0.0871 -12.77023,0.22089 -16.80965,1.10727 -29.68729,3.05317 -44.38296,6.77453 -5.64799,1.43026 -9.96811,2.69833 -15.19914,4.41816 -3.34052,1.09828 -8.41764,2.85364 -8.68521,3.01909 -0.082,0.0507 3.32705,9.32907 7.98597,21.79631 0.0466,0.12496 0.17057,0.13832 0.33123,0.0736 1.11322,-0.44815 6.45699,-2.29745 8.94283,-3.09273 21.39718,-6.84518 43.95735,-10.19531 66.31683,-9.86723 3.14874,0.0461 7.13319,0.15915 8.83245,0.25775 1.69921,0.0987 3.12161,0.15378 3.16493,0.11037 0.0685,-0.0684 1.53237,-23.21444 1.47209,-23.26905 -0.0122,-0.0117 -1.41064,-0.10943 -3.12817,-0.22089 -2.0869,-0.13546 -7.49683,-0.21852 -12.88062,-0.22088 z m 110.81021,28.16581 c -0.10125,0.10911 -11.15095,20.28455 -11.15095,20.36043 0,0.0184 0.507,0.31643 1.14084,0.66267 11.62887,6.35285 24.79463,15.55676 35.14571,24.5945 10.91024,9.52593 21.73966,21.23542 29.47825,31.81082 0.65274,0.89212 0.79099,1.01157 1.03047,0.84681 1.13402,-0.7802 18.39736,-13.76959 18.40089,-13.84358 0.005,-0.1 -3.33561,-4.52525 -4.74744,-6.29593 -5.64395,-7.07831 -10.59767,-12.59168 -17.26005,-19.25582 -10.6531,-10.65598 -20.6627,-18.9637 -33.01119,-27.39263 -6.60131,-4.50599 -18.71149,-11.82683 -19.02653,-11.48727 z m -274.762206,39.94759 -2.428914,2.57732 c -21.579098,22.69359 -38.068397,49.23025 -48.467963,78.05431 -0.50904,1.41091 -0.957247,2.67589 -0.993643,2.83498 -0.04781,0.20904 2.956962,1.31003 10.78292,3.93954 5.956638,2.00143 10.92488,3.63791 11.040538,3.64502 0.115645,0.007 0.916879,-1.94564 1.803285,-4.34458 9.098432,-24.62317 23.187184,-47.25662 41.659643,-66.93523 l 3.05453,-3.27681 -8.206806,-8.24726 z m 388.222146,115.167 -9.89972,1.91451 c -5.44839,1.06994 -10.56998,2.07187 -11.40857,2.20908 -1.04711,0.17137 -1.54564,0.35016 -1.54564,0.55228 0,0.16187 0.23325,1.63204 0.51522,3.2768 2.70275,15.76547 3.28356,34.63258 1.69287,55.26394 -0.7281,9.44363 -2.34823,21.04449 -3.90099,28.01857 -0.23345,1.0486 -0.37949,1.97667 -0.33118,2.02499 0.0483,0.0483 5.12585,1.1561 11.29809,2.46683 6.17232,1.31067 11.30751,2.37915 11.3718,2.39315 0.0641,0.014 0.45734,-1.73307 0.88322,-3.90272 3.35867,-17.11028 4.82653,-33.18977 4.85786,-53.27572 0.0219,-14.08945 -0.79161,-24.35571 -2.87056,-36.96537 z m -427.268845,64.06345 -0.772838,0.11039 c -0.421858,0.0612 -5.59823,0.67716 -11.482161,1.36226 -5.883927,0.68512 -10.759171,1.30169 -10.819725,1.36228 -0.141991,0.142 0.252313,2.91986 1.140854,8.32086 4.869392,29.59836 15.038358,56.25732 31.539139,82.61977 0.450701,0.72005 0.931445,1.27763 1.06725,1.25182 0.361709,-0.0685 19.423106,-12.2036 19.431349,-12.3709 0.0036,-0.0779 -0.796734,-1.40341 -1.766487,-2.94542 -4.266677,-6.78447 -9.935035,-17.45299 -13.064635,-24.5945 -7.52905,-17.18062 -12.488823,-34.71382 -15.051936,-53.27567 z M 462.40066,926.44149 c -0.46898,0.009 -22.08567,5.38002 -22.2283,5.52269 -0.098,0.0981 22.04129,90.06142 22.37549,91.01382 0.40286,1.1482 3.73284,10.5298 13.56323,8.9156 10.95786,-2.3434 9.8458,-14.6677 8.99628,-14.4751 -0.11284,0.025 -5.02627,-20.61508 -11.18774,-45.58033 -8.79763,-35.64656 -11.26829,-45.40142 -11.51896,-45.39668 z M 143.91794,953.1714 c -0.40943,0.0131 -1.21588,1.3276 -6.29312,9.64634 -3.31435,5.43031 -6.03549,9.92123 -6.03549,9.9777 0,0.13674 3.42858,2.19027 7.06593,4.27089 22.35182,12.78549 47.08561,21.82095 72.42596,26.43487 3.59043,0.654 5.67261,1.0064 11.04051,1.804 0.69401,0.1031 1.36954,0.2073 1.50889,0.2212 0.31484,0.031 0.24386,0.6279 1.87691,-11.45018 0.75094,-5.5542 1.40492,-10.43428 1.47207,-10.86126 0.11781,-0.74877 0.0863,-0.77677 -0.58887,-0.88362 -0.38487,-0.0607 -2.68651,-0.4127 -5.11543,-0.77322 -22.30454,-3.3101 -45.25895,-10.90321 -65.32317,-21.57538 -3.55401,-1.89038 -10.16752,-5.64292 -11.85018,-6.73769 -0.0531,-0.0345 -0.12551,-0.0755 -0.18401,-0.0737 z m 214.22322,9.09404 c -1.98095,0.13013 -4.60205,1.01767 -10.4517,3.12954 -11.29964,4.0795 -24.13159,8.26507 -29.91986,9.75681 -0.83741,0.21582 -1.5445,0.50692 -1.54566,0.62592 -0.002,0.21503 5.72469,22.22722 5.81468,22.34847 0.0552,0.075 6.34708,-1.70267 10.2677,-2.90858 5.09669,-1.5677 13.67295,-4.44246 19.79936,-6.62722 l 6.07232,-2.17225 22.30187,8.98356 c 18.14341,7.31671 22.30098,8.95081 22.41231,8.68881 0.9061,-2.1322 8.61707,-21.32757 8.57483,-21.35419 -0.38803,-0.24492 -49.13929,-19.77601 -49.90327,-19.99221 -1.25781,-0.35596 -2.23399,-0.55669 -3.42258,-0.47866 z"
+ style="opacity:0.5;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:5.88958931;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:nodetypes="ccssccssscccccccssscccssscccscsssccccccssssssssssccccscssccsscccsscsscsssssccsccsscssssccscccccss" />
+ </g>
+</svg>
diff --git a/art/conversations_mono.svg b/art/conversations_mono.svg
new file mode 100644
index 00000000..b150ab4b
--- /dev/null
+++ b/art/conversations_mono.svg
@@ -0,0 +1,400 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="512"
+ height="512"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="conversations_mono.svg"
+ inkscape:export-filename="/home/diesys/diesys/grafica/conversation/conversation_notification.png"
+ inkscape:export-xdpi="4.3945312"
+ inkscape:export-ydpi="4.3945312">
+ <defs
+ id="defs4">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3913">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3915" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop3917" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3818">
+ <stop
+ style="stop-color:#669900;stop-opacity:1"
+ offset="0"
+ id="stop3820" />
+ <stop
+ style="stop-color:#99cc00;stop-opacity:1"
+ offset="1"
+ id="stop3822" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3824"
+ cx="212.07048"
+ cy="1045.9178"
+ fx="212.07048"
+ fy="1045.9178"
+ r="238.57143"
+ gradientTransform="matrix(1.9491621,-0.90817722,0.65829208,1.4128498,-879.63121,-248.98648)"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3913"
+ id="radialGradient3919"
+ cx="362.98563"
+ cy="379.77524"
+ fx="362.98563"
+ fy="379.77524"
+ r="139.95312"
+ gradientTransform="matrix(1.3800477,1.0445431,-1.3325077,1.7605059,339.09383,-577.83938)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="-155.75885"
+ x2="114.59022"
+ y1="35.545681"
+ x1="114.55434"
+ id="linearGradient3794"
+ xlink:href="#linearGradient3788"
+ inkscape:collect="always" />
+ <linearGradient
+ id="linearGradient3788">
+ <stop
+ id="stop3790"
+ offset="0"
+ style="stop-color:#1eed00;stop-opacity:1;" />
+ <stop
+ id="stop3792"
+ offset="1"
+ style="stop-color:#abff28;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3821">
+ <stop
+ style="stop-color:#ff283d;stop-opacity:1;"
+ offset="0"
+ id="stop3823" />
+ <stop
+ style="stop-color:#ff28ae;stop-opacity:1;"
+ offset="1"
+ id="stop3825" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4543">
+ <stop
+ style="stop-color:#2e45bf;stop-opacity:1;"
+ offset="0"
+ id="stop4545" />
+ <stop
+ style="stop-color:#28a7ff;stop-opacity:1;"
+ offset="1"
+ id="stop4547" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4098">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop4100" />
+ <stop
+ style="stop-color:#e6e6e6;stop-opacity:1"
+ offset="1"
+ id="stop4102" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4098"
+ id="linearGradient3833"
+ x1="273.81851"
+ y1="764.74677"
+ x2="304.14023"
+ y2="936.47272"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4098"
+ id="linearGradient3853"
+ gradientUnits="userSpaceOnUse"
+ x1="273.81851"
+ y1="764.74677"
+ x2="304.14023"
+ y2="936.47272" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3863"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,170.11831)"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3866"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,170.11831)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3870"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,170.11831)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3913"
+ id="radialGradient3873"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.3800477,1.0445431,-1.3325077,1.7605059,339.09383,-577.83938)"
+ cx="321.75275"
+ cy="386.38751"
+ fx="321.75275"
+ fy="386.38751"
+ r="139.95312" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3818"
+ id="radialGradient3880"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2253203,-0.54206726,0.43090148,0.97403458,-466.4135,-370.24387)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3913"
+ id="radialGradient3883"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4430075,-0.63865195,0.50745433,1.1475866,-594.40824,44.803037)"
+ cx="262.33273"
+ cy="945.23846"
+ fx="262.33273"
+ fy="945.23846"
+ r="185.49754" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.49497475"
+ inkscape:cx="396.66929"
+ inkscape:cy="192.73156"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="636"
+ inkscape:window-height="1161"
+ inkscape:window-x="1280"
+ inkscape:window-y="18"
+ inkscape:window-maximized="0"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-to-guides="true"
+ inkscape:snap-grids="false"
+ inkscape:object-paths="true"
+ inkscape:object-nodes="false"
+ inkscape:snap-nodes="false">
+ <sodipodi:guide
+ orientation="1,0"
+ position="0,534.28571"
+ id="guide3004" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="394.28571,511.42857"
+ id="guide3006" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="511.42857,320"
+ id="guide3008" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="401.42857,0"
+ id="guide3010" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="17.142857,258.57143"
+ id="guide3012" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="327.14286,494.28571"
+ id="guide3014" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="324.28571,17.142857"
+ id="guide3016" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="494.28571,237.14286"
+ id="guide3018" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="255.71429,302.85714"
+ id="guide3022" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="495.71429,255.71429"
+ id="guide3024" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="660,-315"
+ id="guide3904" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="554.28571,475.71429"
+ id="guide3931" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="581.42857,244.28571"
+ id="guide3933" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-540.36218)"
+ style="display:inline">
+ <g
+ id="g3971"
+ transform="matrix(1.3691054,0,0,1.2698854,-209.33716,-230.16141)"
+ style="opacity:1">
+ <g
+ id="g3945">
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357000000198px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.40000000000000002;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="text3923">
+ <path
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889"
+ style="font-size:627.96289062000016656px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3943"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357000000198px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="text3927">
+ <path
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889"
+ style="font-size:627.96289062000016656px;font-weight:bold;letter-spacing:-1.37633753000000003px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3940"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ transform="translate(80,0)"
+ id="g3951">
+ <g
+ id="g3953"
+ style="font-size:218.04266357000000198px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.40000000000000002;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path3955"
+ style="font-size:627.96289062000016656px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889" />
+ </g>
+ <g
+ id="g3957"
+ style="font-size:218.04266357000000198px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path3959"
+ style="font-size:627.96289062000016656px;font-weight:bold;letter-spacing:-1.37633753000000003px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889" />
+ </g>
+ </g>
+ <g
+ id="g3961"
+ transform="translate(160,0)">
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357000000198px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.40000000000000002;fill:#000000;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="g3963">
+ <path
+ d="m 261.11551,789.25169 c 0,12.55924 18.8389,18.83889 31.39814,18.83889 10.67536,0 18.83889,-8.16353 18.83889,-18.83889 0,-12.55925 -18.8389,-18.83889 -31.39815,-18.83889 -10.67536,0 -18.83888,8.16353 -18.83888,18.83889"
+ style="font-size:627.96289062000016656px;font-weight:bold;fill:#000000;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3965"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="matrix(0.97180599,0.00206436,-0.02018243,1.0289691,0,0)"
+ style="font-size:218.04266357000000198px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Rockwell;-inkscape-font-specification:Rockwell"
+ id="g3967">
+ <path
+ d="m 256.13214,781.18791 c 0,12.55924 18.8389,18.83888 31.39814,18.83888 10.67536,0 18.83889,-8.16352 18.83889,-18.83888 0,-12.55925 -18.8389,-18.83889 -31.39814,-18.83889 -10.67536,0 -18.83889,8.16353 -18.83889,18.83889"
+ style="font-size:627.96289062000016656px;font-weight:bold;letter-spacing:-1.37633753000000003px;fill:#ffffff;font-family:Pecita;-inkscape-font-specification:Pecita"
+ id="path3969"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ </g>
+ <path
+ sodipodi:nodetypes="ccsssscc"
+ inkscape:connector-curvature="0"
+ id="path4129"
+ d="m -179.24078,998.26318 -26.56173,-107.46309 c 16.8897,-27.15918 22.70855,-59.05025 22.70855,-93.16236 0,-99.1504 -82.19661,-179.50071 -183.57099,-179.50071 -101.37434,0 -183.57094,80.35031 -183.57094,179.50071 0,99.15039 82.1966,179.56005 183.57094,179.56005 33.65679,0 58.24146,-6.91097 89.32491,-18.47 z"
+ style="opacity:0.29999999999999999;fill:none;stroke:#ffffff;stroke-width:20;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:80, 80;stroke-dashoffset:0;fill-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccsssscc"
+ inkscape:connector-curvature="0"
+ id="path3845"
+ d="M 478.64112,1025.218 447.36049,898.60749 c 19.89028,-31.99834 26.74288,-69.57172 26.74288,-109.76189 0,-116.81686 -96.79943,-211.48385 -216.18374,-211.48385 -119.38425,0 -216.183656,94.66699 -216.183656,211.48385 0,116.81685 96.799406,211.5536 216.183656,211.5536 39.63617,0 68.58847,-8.14219 105.19417,-21.76075 z"
+ style="opacity:0;fill:none;stroke:#000000;stroke-width:23.55835724;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:94.23343197, 94.23343197;stroke-dashoffset:0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3855"
+ d="m 253.18246,561.05889 c -5.38379,-0.002 -10.7413,0.0871 -12.77023,0.22089 -16.80965,1.10727 -29.68729,3.05317 -44.38296,6.77453 -5.64799,1.43026 -9.96811,2.69833 -15.19914,4.41816 -3.34052,1.09828 -8.41764,2.85364 -8.68521,3.01909 -0.082,0.0507 3.32705,9.32907 7.98597,21.79631 0.0466,0.12496 0.17057,0.13832 0.33123,0.0736 1.11322,-0.44815 6.45699,-2.29745 8.94283,-3.09273 21.39718,-6.84518 43.95735,-10.19531 66.31683,-9.86723 3.14874,0.0461 7.13319,0.15915 8.83245,0.25775 1.69921,0.0987 3.12161,0.15378 3.16493,0.11037 0.0685,-0.0684 1.53237,-23.21444 1.47209,-23.26905 -0.0122,-0.0117 -1.41064,-0.10943 -3.12817,-0.22089 -2.0869,-0.13546 -7.49683,-0.21852 -12.88062,-0.22088 z m 110.81021,28.16581 c -0.10125,0.10911 -11.15095,20.28455 -11.15095,20.36043 0,0.0184 0.507,0.31643 1.14084,0.66267 11.62887,6.35285 24.79463,15.55676 35.14571,24.5945 10.91024,9.52593 21.73966,21.23542 29.47825,31.81082 0.65274,0.89212 0.79099,1.01157 1.03047,0.84681 1.13402,-0.7802 18.39736,-13.76959 18.40089,-13.84358 0.005,-0.1 -3.33561,-4.52525 -4.74744,-6.29593 -5.64395,-7.07831 -10.59767,-12.59168 -17.26005,-19.25582 -10.6531,-10.65598 -20.6627,-18.9637 -33.01119,-27.39263 -6.60131,-4.50599 -18.71149,-11.82683 -19.02653,-11.48727 z m -274.762206,39.94759 -2.428914,2.57732 c -21.579098,22.69359 -38.068397,49.23025 -48.467963,78.05431 -0.50904,1.41091 -0.957247,2.67589 -0.993643,2.83498 -0.04781,0.20904 2.956962,1.31003 10.78292,3.93954 5.956638,2.00143 10.92488,3.63791 11.040538,3.64502 0.115645,0.007 0.916879,-1.94564 1.803285,-4.34458 9.098432,-24.62317 23.187184,-47.25662 41.659643,-66.93523 l 3.05453,-3.27681 -8.206806,-8.24726 z m 388.222146,115.167 -9.89972,1.91451 c -5.44839,1.06994 -10.56998,2.07187 -11.40857,2.20908 -1.04711,0.17137 -1.54564,0.35016 -1.54564,0.55228 0,0.16187 0.23325,1.63204 0.51522,3.2768 2.70275,15.76547 3.28356,34.63258 1.69287,55.26394 -0.7281,9.44363 -2.34823,21.04449 -3.90099,28.01857 -0.23345,1.0486 -0.37949,1.97667 -0.33118,2.02499 0.0483,0.0483 5.12585,1.1561 11.29809,2.46683 6.17232,1.31067 11.30751,2.37915 11.3718,2.39315 0.0641,0.014 0.45734,-1.73307 0.88322,-3.90272 3.35867,-17.11028 4.82653,-33.18977 4.85786,-53.27572 0.0219,-14.08945 -0.79161,-24.35571 -2.87056,-36.96537 z m -427.268845,64.06345 -0.772838,0.11039 c -0.421858,0.0612 -5.59823,0.67716 -11.482161,1.36226 -5.883927,0.68512 -10.759171,1.30169 -10.819725,1.36228 -0.141991,0.142 0.252313,2.91986 1.140854,8.32086 4.869392,29.59836 15.038358,56.25732 31.539139,82.61977 0.450701,0.72005 0.931445,1.27763 1.06725,1.25182 0.361709,-0.0685 19.423106,-12.2036 19.431349,-12.3709 0.0036,-0.0779 -0.796734,-1.40341 -1.766487,-2.94542 -4.266677,-6.78447 -9.935035,-17.45299 -13.064635,-24.5945 -7.52905,-17.18062 -12.488823,-34.71382 -15.051936,-53.27567 z M 462.40066,926.44149 c -0.46898,0.009 -22.08567,5.38002 -22.2283,5.52269 -0.098,0.0981 22.04129,90.06142 22.37549,91.01382 0.40286,1.1482 3.73284,10.5298 13.56323,8.9156 10.95786,-2.3434 9.8458,-14.6677 8.99628,-14.4751 -0.11284,0.025 -5.02627,-20.61508 -11.18774,-45.58033 -8.79763,-35.64656 -11.26829,-45.40142 -11.51896,-45.39668 z M 143.91794,953.1714 c -0.40943,0.0131 -1.21588,1.3276 -6.29312,9.64634 -3.31435,5.43031 -6.03549,9.92123 -6.03549,9.9777 0,0.13674 3.42858,2.19027 7.06593,4.27089 22.35182,12.78549 47.08561,21.82095 72.42596,26.43487 3.59043,0.654 5.67261,1.0064 11.04051,1.804 0.69401,0.1031 1.36954,0.2073 1.50889,0.2212 0.31484,0.031 0.24386,0.6279 1.87691,-11.45018 0.75094,-5.5542 1.40492,-10.43428 1.47207,-10.86126 0.11781,-0.74877 0.0863,-0.77677 -0.58887,-0.88362 -0.38487,-0.0607 -2.68651,-0.4127 -5.11543,-0.77322 -22.30454,-3.3101 -45.25895,-10.90321 -65.32317,-21.57538 -3.55401,-1.89038 -10.16752,-5.64292 -11.85018,-6.73769 -0.0531,-0.0345 -0.12551,-0.0755 -0.18401,-0.0737 z m 214.22322,9.09404 c -1.98095,0.13013 -4.60205,1.01767 -10.4517,3.12954 -11.29964,4.0795 -24.13159,8.26507 -29.91986,9.75681 -0.83741,0.21582 -1.5445,0.50692 -1.54566,0.62592 -0.002,0.21503 5.72469,22.22722 5.81468,22.34847 0.0552,0.075 6.34708,-1.70267 10.2677,-2.90858 5.09669,-1.5677 13.67295,-4.44246 19.79936,-6.62722 l 6.07232,-2.17225 22.30187,8.98356 c 18.14341,7.31671 22.30098,8.95081 22.41231,8.68881 0.9061,-2.1322 8.61707,-21.32757 8.57483,-21.35419 -0.38803,-0.24492 -49.13929,-19.77601 -49.90327,-19.99221 -1.25781,-0.35596 -2.23399,-0.55669 -3.42258,-0.47866 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:5.88958930999999986;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:nodetypes="ccssccssscccccccssscccssscccscsssccccccssssssssssccccscssccsscccsscsscsssssccsccsscssssccscccccss" />
+ </g>
+</svg>
diff --git a/art/ic_action_send_now.svg b/art/ic_action_send_now.svg
new file mode 100644
index 00000000..6bde9158
--- /dev/null
+++ b/art/ic_action_send_now.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg3621"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ width="96"
+ height="96"
+ sodipodi:docname="ic_action_send_now.svg"
+ inkscape:export-filename="/home/daniel/workspace/Conversations/res/drawable-xxhdpi/ic_action_send_now_online.png"
+ inkscape:export-xdpi="154.28572"
+ inkscape:export-ydpi="154.28572">
+ <metadata
+ id="metadata3627">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs3625" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1916"
+ inkscape:window-height="1161"
+ id="namedview3623"
+ showgrid="true"
+ showguides="true"
+ inkscape:zoom="1"
+ inkscape:cx="47.28873"
+ inkscape:cy="43.262706"
+ inkscape:window-x="0"
+ inkscape:window-y="18"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg3621">
+ <inkscape:grid
+ type="xygrid"
+ id="grid3631" />
+ </sodipodi:namedview>
+ <path
+ style="fill:#e51c28;fill-opacity:0.627451;stroke:none"
+ d="M 20.012575,21.028577 76,49 20.012575,77.028577 26,52 58.012575,49.028577 26,46 z"
+ id="path3633"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc"
+ inkscape:export-filename="/home/daniel/workspace/Conversations/res/drawable-mdpi/ic_action_send_now_dnd.png"
+ inkscape:export-xdpi="51.42857"
+ inkscape:export-ydpi="51.42857" />
+</svg>
diff --git a/art/ic_received_indicator.svg b/art/ic_received_indicator.svg
new file mode 100644
index 00000000..d9378c60
--- /dev/null
+++ b/art/ic_received_indicator.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="95"
+ height="95"
+ id="Yes_check"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="ic_received_indicator.svg">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1233"
+ inkscape:window-height="828"
+ id="namedview8"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:zoom="5.04"
+ inkscape:cx="26.829268"
+ inkscape:cy="37.489149"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Yes_check"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0" />
+ <defs
+ id="defs1373">
+ <linearGradient
+ id="linearGradient2250">
+ <stop
+ style="stop-color:#008700;stop-opacity:1"
+ offset="0"
+ id="stop2252" />
+ <stop
+ style="stop-color:#006f00;stop-opacity:1"
+ offset="1"
+ id="stop2254" />
+ </linearGradient>
+ </defs>
+ <path
+ d="m 2.3894499,61.412131 c 0,0 16.7473651,20.271938 22.3528491,26.154483 3.648598,3.026816 12.878061,3.83429 14.880462,0 1.64903,-2.636163 2.380404,-5.8348 2.991819,-7.931771 C 49.920898,54.575958 72.297563,22.337321 92.321082,10.50894 96.814837,5.2377522 86.327596,3.5063483 77.217442,6.9958109 63.487006,12.254946 34.107717,59.529917 29.270873,69.192545 22.40265,70.841418 12.518762,52.447046 12.518762,52.447046 7.3805037,52.552428 1.8841059,52.071763 2.3894499,61.412131 z"
+ style="fill:#249b25;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.29981154;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="check"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccscsccc" />
+</svg>
diff --git a/art/ic_secure_indicator.xcf b/art/ic_secure_indicator.xcf
new file mode 100644
index 00000000..a9069c9b
--- /dev/null
+++ b/art/ic_secure_indicator.xcf
Binary files differ
diff --git a/art/logo.png b/art/logo.png
new file mode 100644
index 00000000..a8ab6176
--- /dev/null
+++ b/art/logo.png
Binary files differ
diff --git a/art/render.rb b/art/render.rb
new file mode 100755
index 00000000..2847891d
--- /dev/null
+++ b/art/render.rb
@@ -0,0 +1,22 @@
+#!/bin/env ruby
+resolutions={
+ 'mdpi'=> 1,
+ 'hdpi' => 1.5,
+ 'xhdpi' => 2,
+ 'xxhdpi' => 3,
+ }
+images = {
+ 'conversations.svg' => ['ic_launcher', 48],
+ 'conversations_baloon.svg' => ['ic_activity', 32],
+ 'conversations_mono.svg' => ['ic_notification', 24],
+ 'ic_received_indicator.svg' => ['ic_received_indicator', 12],
+ }
+images.each do |source, result|
+ resolutions.each do |name, factor|
+ size = factor * result[1]
+ path = "../res/drawable-#{name}/#{result[0]}.png"
+ cmd = "inkscape -e #{path} -C -h #{size} -w #{size} #{source}"
+ puts cmd
+ system cmd
+ end
+end
diff --git a/build.gradle b/build.gradle
index 5941beaf..68b1b2f2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,77 +1,118 @@
-apply plugin: 'java'
-apply plugin: 'eclipse'
-apply plugin: 'osgi'
-apply plugin: 'nexus'
-
+// Top-level build file where you can add configuration options common to all
+// sub-projects/modules.
buildscript {
- repositories {
- jcenter()
- mavenLocal()
- mavenCentral()
- }
-
- dependencies {
- classpath 'org.gradle.api.plugins:gradle-nexus-plugin:0.7.1'
- }
+ repositories {
+ jcenter()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.12.2'
+ }
}
-group = 'de.measite.minidns'
-description = "A minimal DNS client library with support for A, AAAA, NS and SRV records"
-sourceCompatibility = 1.7
-version = 'git tag --points-at HEAD'.execute().text.trim()
-isSNAPSHOT = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim() == 'master'
-
-if (isSNAPSHOT) {
- version = version + '-SNAPSHOT'
+allprojects {
+ repositories {
+ jcenter()
+ mavenCentral()
+ }
}
+apply plugin: 'com.android.application'
+
repositories {
- mavenLocal()
+ jcenter()
mavenCentral()
+ maven {
+ url "http://jitsi.github.com/otr4j/repository/"
+ }
}
-nexus {
- attachSources = true
- attachTests = false
- attachJavadoc = true
- sign = true
+dependencies {
+ compile project(':libs/minidns')
+ compile project(':libs/openpgp-api-lib')
+ compile project(':libs/MemorizingTrustManager')
+ compile 'com.android.support:support-v13:19.1.0'
+ compile 'org.bouncycastle:bcprov-jdk15on:1.50'
+ compile 'net.java:otr4j:0.21'
+ compile fileTree(dir: 'libs', include: ['*.jar'])
}
-modifyPom {
- project {
- name 'minidns'
- description 'Minimal DNS library for java and android systems'
- url 'https://github.com/rtreffer/minidns'
- inceptionYear '2014'
+android {
+ compileSdkVersion 19
+ buildToolsVersion "19.1"
- scm {
- url 'https://github.com/rtreffer/minidns'
- connection 'scm:https://github.com/rtreffer/minidns'
- developerConnection 'scm:git://github.com/rtreffer/minidns.git'
- }
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 19
+ versionCode 32
+ versionName "0.8-alpha"
+ }
- licenses {
- license {
- name 'The Apache Software License, Version 2.0'
- url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
- distribution 'repo'
- }
- }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
- developers {
- developer {
- id 'rtreffer'
- name 'Rene Treffer'
- email 'treffer@measite.de'
- }
- developer {
- id 'flow'
- name 'Florian Schmaus'
- email 'flow@geekplace.eu'
- }
- }
- }
-}
+ //
+ // To sign release builds, create the file `gradle.properties` in
+ // $HOME/.gradle or in your project directory with this content:
+ //
+ // mStoreFile=/path/to/key.store
+ // mStorePassword=xxx
+ // mKeyAlias=alias
+ // mKeyPassword=xxx
+ //
+ if (project.hasProperty('mStoreFile') &&
+ project.hasProperty('mStorePassword') &&
+ project.hasProperty('mKeyAlias') &&
+ project.hasProperty('mKeyPassword')) {
+ signingConfigs {
+ release {
+ storeFile file(mStoreFile)
+ storePassword mStorePassword
+ keyAlias mKeyAlias
+ keyPassword mKeyPassword
+ }
+ }
+ buildTypes.release.signingConfig = signingConfigs.release
+ } else {
+ buildTypes.release.signingConfig = null
+ }
-dependencies {
-} \ No newline at end of file
+ buildTypes {
+ release {
+ runProguard true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ applicationVariants.all { variant ->
+ def fileName = variant.packageApplication.outputFile.name.replace(".apk",
+ "-" + defaultConfig.versionName + ".apk")
+ variant.packageApplication.outputFile = new
+ File(variant.packageApplication.outputFile.parent, fileName)
+ if (variant.zipAlign) {
+ if (variant.name.equals('release')) {
+ variant.outputFile = new File(variant.outputFile.parent,
+ rootProject.name + "-" + defaultConfig.versionName + ".apk")
+ }
+ }
+ }
+ }
+
+ lintOptions {
+ disable 'MissingTranslation', 'InvalidPackage'
+ }
+
+ subprojects {
+
+ afterEvaluate {
+ if (getPlugins().hasPlugin('android') ||
+ getPlugins().hasPlugin('android-library')) {
+
+ configure(android.lintOptions) {
+ disable 'AndroidGradlePluginVersion', 'MissingTranslation'
+ }
+ }
+
+ }
+ }
+}
diff --git a/gradle.properties.example b/gradle.properties.example
deleted file mode 100644
index 68ffc418..00000000
--- a/gradle.properties.example
+++ /dev/null
@@ -1,21 +0,0 @@
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-#
-# GPG settings
-#
-
-# gpg key id
-#signing.keyId=DEADBEEF
-# the gpg key passphrase
-#signing.password=correcthorsebatterystaple
-# gpg keyring (this is the default gnupg keyring containing private keys)
-#signing.secretKeyRingFile=/home/ubuntu/.gnupg/secring.gpg
-
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-#
-# nexus settings
-#
-
-# the nexus username used for log in
-#nexusUsername=ubuntu
-# the nexus password
-#nexusPassword=correcthorsebatterystaple
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..8c0fb64a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..1e61d1fd
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..91a7e269
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/MemorizingTrustManager/.gitignore b/libs/MemorizingTrustManager/.gitignore
new file mode 100644
index 00000000..c642de10
--- /dev/null
+++ b/libs/MemorizingTrustManager/.gitignore
@@ -0,0 +1,11 @@
+bin
+build
+gen
+local.properties
+example/bin
+example/gen
+tags
+.project
+.classpath
+.gradle
+.*.swp
diff --git a/libs/MemorizingTrustManager/AndroidManifest.xml b/libs/MemorizingTrustManager/AndroidManifest.xml
new file mode 100644
index 00000000..c125afe4
--- /dev/null
+++ b/libs/MemorizingTrustManager/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="de.duenndns.ssl"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <application android:label="MemorizingTrustManager">
+ <activity android:name="de.duenndns.ssl.MemorizingActivity"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+ </application>
+</manifest>
diff --git a/libs/MemorizingTrustManager/LICENSE.txt b/libs/MemorizingTrustManager/LICENSE.txt
new file mode 100644
index 00000000..25012507
--- /dev/null
+++ b/libs/MemorizingTrustManager/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT license.
+
+Copyright (c) 2010 Georg Lukas <georg@op-co.de>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/MemorizingTrustManager/README.mdwn b/libs/MemorizingTrustManager/README.mdwn
new file mode 100644
index 00000000..c48f38de
--- /dev/null
+++ b/libs/MemorizingTrustManager/README.mdwn
@@ -0,0 +1,125 @@
+# MemorizingTrustManager - Private Cloud Support for Your App
+
+MemorizingTrustManager (MTM) is a project to enable smarter and more secure use
+of SSL on Android. If it encounters an unknown SSL certificate, it asks the
+user whether to accept the certificate once, permanently or to abort the
+connection. This is a step in preventing man-in-the-middle attacks by blindly
+accepting any invalid, self-signed and/or expired certificates.
+
+MTM is aimed at providing seamless integration into your Android application,
+and the source code is available under the MIT license.
+
+## Screenshots
+
+![MemorizingTrustManager dialog](mtm-screenshot.png)
+![MemorizingTrustManager notification](mtm-notification.png)
+![MemorizingTrustManager server name dialog](mtm-servername.png)
+
+## Status
+
+MemorizingTrustManager is in production use in the
+[yaxim XMPP client](https://yaxim.org/). It is usable and easy to integrate,
+though it does not yet support hostname validation (the Java API makes it
+**hard** to integrate).
+
+## Integration
+
+MTM is easy to integrate into your own application. Follow these steps or have
+a look into the demo application in the `example` directory.
+
+### 1. Add MTM to your project
+
+Download the MTM source from GitHub, or add it as a
+[git submodule](http://git-scm.com/docs/git-submodule):
+
+ # plain download:
+ git clone https://github.com/ge0rg/MemorizingTrustManager
+ # submodule:
+ git submodule add https://github.com/ge0rg/MemorizingTrustManager
+
+Then add a library project dependency to `default.properties`:
+
+ android.library.reference.1=MemorizingTrustManager
+
+### 2. Add the MTM (popup) Activity to your manifest
+
+Edit your `AndroidManifest.xml` and add the MTM activity element right before the
+end of your closing `</application>` tag.
+
+ ...
+ <activity android:name="de.duenndns.ssl.MemorizingActivity"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar"
+ />
+ </application>
+ </manifest>
+
+### 3. Hook MTM as the default TrustManager for your connection type
+
+Hooking MemorizingTrustmanager in HTTPS connections:
+
+ // register MemorizingTrustManager for HTTPS
+ SSLContext sc = SSLContext.getInstance("TLS");
+ MemorizingTrustManager mtm = new MemorizingTrustManager(this);
+ sc.init(null, new X509TrustManager[] { mtm }, new java.security.SecureRandom());
+ HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ HttpsURLConnection.setDefaultHostnameVerifier(
+ mtm.wrapHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier()));
+
+
+Or, for aSmack you can use `setCustomSSLContext()`:
+
+ org.jivesoftware.smack.ConnectionConfiguration connectionConfiguration = …
+ SSLContext sc = SSLContext.getInstance("TLS");
+ MemorizingTrustManager mtm = new MemorizingTrustManager(this);
+ sc.init(null, new X509TrustManager[] { mtm }, new java.security.SecureRandom());
+ connectionConfiguration.setCustomSSLContext(sc);
+ connectionConfiguration.setHostnameVerifier(
+ mtm.wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier()));
+
+By default, MTM falls back to the system `TrustManager` before asking the user.
+If you do not trust the establishment, you can enforce a dialog on *every new
+connection* by supplying a `defaultTrustManager = null` parameter to the
+constructor:
+
+ MemorizingTrustManager mtm = new MemorizingTrustManager(this, null);
+
+If you want to use a different underlying `TrustManager`, like
+[AndroidPinning](https://github.com/moxie0/AndroidPinning), just supply that to
+MTM's constructor:
+
+ X509TrustManager pinning = new PinningTrustManager(SystemKeyStore.getInstance(),
+ new String[] {"f30012bbc18c231ac1a44b788e410ce754182513"}, 0);
+ MemorizingTrustManager mtm = new MemorizingTrustManager(this, pinning);
+
+### 4. Profit!
+
+### Logging
+
+MTM uses java.util.logging (JUL) for logging purposes. If you have not
+configured a Handler for JUL, then Android will by default log all
+messages of Level.INFO or higher. In order to get also the debug log
+messages (those with Level.FINE or lower) you need to configure a
+Handler accordingly. The MTM example project contains
+de.duenndns.mtmexample.JULHandler, which allows to enable and disable
+debug logging at runtime.
+
+## Alternatives
+
+MemorizingTrustManager is not the only one out there.
+
+[**NetCipher**](https://guardianproject.info/code/netcipher/) is an Android
+library made by the [Guardian Project](https://guardianproject.info/) to
+improve network security for mobile apps. It comes with a StrongTrustManager
+to do more thorough certificate checks, an independent Root CA store, and code
+to easily route your traffic through
+[the Tor network](https://www.torproject.org/) using [Orbot](https://guardianproject.info/apps/orbot/).
+
+[**AndroidPinning**](https://github.com/moxie0/AndroidPinning) is another Android
+library, written by [Moxie Marlinspike](http://www.thoughtcrime.org/) to allow
+pinning of server certificates, improving security against government-scale
+MitM attacks. Use this if your app is made to communicate with a specific
+server!
+
+## Contribute
+
+Please [help translating MTM into more languages](https://translations.launchpad.net/yaxim/master/+pots/mtm/)!
diff --git a/libs/MemorizingTrustManager/ant.properties b/libs/MemorizingTrustManager/ant.properties
new file mode 100644
index 00000000..ee52d86d
--- /dev/null
+++ b/libs/MemorizingTrustManager/ant.properties
@@ -0,0 +1,17 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked in Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
diff --git a/libs/MemorizingTrustManager/build.gradle b/libs/MemorizingTrustManager/build.gradle
new file mode 100644
index 00000000..aa022a93
--- /dev/null
+++ b/libs/MemorizingTrustManager/build.gradle
@@ -0,0 +1,32 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.7.+'
+ }
+}
+
+apply plugin: 'android-library'
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "19.1"
+ defaultConfig {
+ minSdkVersion 7
+ targetSdkVersion 19
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java.srcDirs = ['src']
+ resources.srcDirs = ['src']
+ aidl.srcDirs = ['src']
+ renderscript.srcDirs = ['src']
+ res.srcDirs = ['res']
+ assets.srcDirs = ['assets']
+ }
+ }
+
+}
diff --git a/libs/MemorizingTrustManager/build.xml b/libs/MemorizingTrustManager/build.xml
new file mode 100644
index 00000000..06cf485c
--- /dev/null
+++ b/libs/MemorizingTrustManager/build.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="MemorizingTrustManager" default="help">
+
+ <!-- The local.properties file is created and updated by the 'android' tool.
+ It contains the path to the SDK. It should *NOT* be checked into
+ Version Control Systems. -->
+ <property file="local.properties" />
+
+ <!-- The ant.properties file can be created by you. It is only edited by the
+ 'android' tool to add properties to it.
+ This is the place to change some Ant specific build properties.
+ Here are some properties you may want to change/update:
+
+ source.dir
+ The name of the source directory. Default is 'src'.
+ out.dir
+ The name of the output directory. Default is 'bin'.
+
+ For other overridable properties, look at the beginning of the rules
+ files in the SDK, at tools/ant/build.xml
+
+ Properties related to the SDK location or the project target should
+ be updated using the 'android' tool with the 'update' action.
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems.
+
+ -->
+ <property file="ant.properties" />
+
+ <!-- if sdk.dir was not set from one of the property file, then
+ get it from the ANDROID_HOME env var.
+ This must be done before we load project.properties since
+ the proguard config can use sdk.dir -->
+ <property environment="env" />
+ <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+ <isset property="env.ANDROID_HOME" />
+ </condition>
+
+ <!-- The project.properties file is created and updated by the 'android'
+ tool, as well as ADT.
+
+ This contains project specific properties such as project target, and library
+ dependencies. Lower level build properties are stored in ant.properties
+ (or in .classpath for Eclipse projects).
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems. -->
+ <loadproperties srcFile="project.properties" />
+
+ <!-- quick check on sdk.dir -->
+ <fail
+ message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+ unless="sdk.dir"
+ />
+
+ <!--
+ Import per project custom build rules if present at the root of the project.
+ This is the place to put custom intermediary targets such as:
+ -pre-build
+ -pre-compile
+ -post-compile (This is typically used for code obfuscation.
+ Compiled code location: ${out.classes.absolute.dir}
+ If this is not done in place, override ${out.dex.input.absolute.dir})
+ -post-package
+ -post-build
+ -pre-clean
+ -->
+ <import file="custom_rules.xml" optional="true" />
+
+ <!-- Import the actual build file.
+
+ To customize existing targets, there are two options:
+ - Customize only one target:
+ - copy/paste the target into this file, *before* the
+ <import> task.
+ - customize it to your needs.
+ - Customize the whole content of build.xml
+ - copy/paste the content of the rules files (minus the top node)
+ into this file, replacing the <import> task.
+ - customize to your needs.
+
+ ***********************
+ ****** IMPORTANT ******
+ ***********************
+ In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+ in order to avoid having your file be overridden by tools such as "android update project"
+ -->
+ <!-- version-tag: 1 -->
+ <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/libs/MemorizingTrustManager/example/AndroidManifest.xml b/libs/MemorizingTrustManager/example/AndroidManifest.xml
new file mode 100644
index 00000000..cdc0450b
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="de.duenndns.mtmexample"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk
+ android:minSdkVersion="3"
+ android:targetSdkVersion="19" />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application android:label="@string/app_name" android:icon="@android:drawable/ic_lock_lock">
+ <activity
+ android:name=".MTMExample"
+ android:configChanges="keyboardHidden|orientation|screenSize|screenLayout"
+ android:label="@string/app_name" >
+
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <!-- ADD THE FOLLOWING TO YOUR MANIFEST: -->
+ <activity android:name="de.duenndns.ssl.MemorizingActivity"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+ </application>
+</manifest>
diff --git a/libs/MemorizingTrustManager/example/ant.properties b/libs/MemorizingTrustManager/example/ant.properties
new file mode 100644
index 00000000..27fcaadd
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/ant.properties
@@ -0,0 +1,18 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked in Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
+application.package=de.duenndns.mtmexample
diff --git a/libs/MemorizingTrustManager/example/build.gradle b/libs/MemorizingTrustManager/example/build.gradle
new file mode 100644
index 00000000..00bfe99e
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'android'
+
+dependencies {
+ compile rootProject
+}
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "19.1"
+ defaultConfig {
+ minSdkVersion 7
+ targetSdkVersion 19
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java.srcDirs = ['src']
+ res.srcDirs = ['res']
+ }
+ }
+
+}
diff --git a/libs/MemorizingTrustManager/example/build.xml b/libs/MemorizingTrustManager/example/build.xml
new file mode 100644
index 00000000..cdc74917
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/build.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="MTMExample" default="help">
+
+ <!-- The local.properties file is created and updated by the 'android' tool.
+ It contains the path to the SDK. It should *NOT* be checked into
+ Version Control Systems. -->
+ <property file="local.properties" />
+
+ <!-- The ant.properties file can be created by you. It is only edited by the
+ 'android' tool to add properties to it.
+ This is the place to change some Ant specific build properties.
+ Here are some properties you may want to change/update:
+
+ source.dir
+ The name of the source directory. Default is 'src'.
+ out.dir
+ The name of the output directory. Default is 'bin'.
+
+ For other overridable properties, look at the beginning of the rules
+ files in the SDK, at tools/ant/build.xml
+
+ Properties related to the SDK location or the project target should
+ be updated using the 'android' tool with the 'update' action.
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems.
+
+ -->
+ <property file="ant.properties" />
+
+ <!-- if sdk.dir was not set from one of the property file, then
+ get it from the ANDROID_HOME env var.
+ This must be done before we load project.properties since
+ the proguard config can use sdk.dir -->
+ <property environment="env" />
+ <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+ <isset property="env.ANDROID_HOME" />
+ </condition>
+
+ <!-- The project.properties file is created and updated by the 'android'
+ tool, as well as ADT.
+
+ This contains project specific properties such as project target, and library
+ dependencies. Lower level build properties are stored in ant.properties
+ (or in .classpath for Eclipse projects).
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems. -->
+ <loadproperties srcFile="project.properties" />
+
+ <!-- quick check on sdk.dir -->
+ <fail
+ message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+ unless="sdk.dir"
+ />
+
+ <!--
+ Import per project custom build rules if present at the root of the project.
+ This is the place to put custom intermediary targets such as:
+ -pre-build
+ -pre-compile
+ -post-compile (This is typically used for code obfuscation.
+ Compiled code location: ${out.classes.absolute.dir}
+ If this is not done in place, override ${out.dex.input.absolute.dir})
+ -post-package
+ -post-build
+ -pre-clean
+ -->
+ <import file="custom_rules.xml" optional="true" />
+
+ <!-- Import the actual build file.
+
+ To customize existing targets, there are two options:
+ - Customize only one target:
+ - copy/paste the target into this file, *before* the
+ <import> task.
+ - customize it to your needs.
+ - Customize the whole content of build.xml
+ - copy/paste the content of the rules files (minus the top node)
+ into this file, replacing the <import> task.
+ - customize to your needs.
+
+ ***********************
+ ****** IMPORTANT ******
+ ***********************
+ In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+ in order to avoid having your file be overridden by tools such as "android update project"
+ -->
+ <!-- version-tag: 1 -->
+ <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/libs/MemorizingTrustManager/example/proguard-project.txt b/libs/MemorizingTrustManager/example/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libs/MemorizingTrustManager/example/project.properties b/libs/MemorizingTrustManager/example/project.properties
new file mode 100644
index 00000000..3692949f
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/project.properties
@@ -0,0 +1,12 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+android.library.reference.1=../
+# Project target.
+target=android-19
diff --git a/libs/MemorizingTrustManager/example/res/layout/mtmexample.xml b/libs/MemorizingTrustManager/example/res/layout/mtmexample.xml
new file mode 100644
index 00000000..dfef58b6
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/res/layout/mtmexample.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" >
+ <EditText
+ android:id="@+id/url"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="HTTPS address"
+ android:text="https://op-co.de/mtm/"
+ android:singleLine="true"
+ />
+ <Button
+ android:id="@+id/connect"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Connect"
+ />
+ <TextView
+ android:id="@+id/content"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Please enter a HTTPS URL and press 'Connect'!"
+ android:textSize="11pt"
+ />
+ <Button
+ android:id="@+id/manage"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Clean up Certificates"
+ android:onClick="onManage"
+ />
+</LinearLayout>
+
diff --git a/libs/MemorizingTrustManager/example/res/values/strings.xml b/libs/MemorizingTrustManager/example/res/values/strings.xml
new file mode 100644
index 00000000..e4f505bc
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">MemorizingTrustManager Example</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/JULHandler.java b/libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/JULHandler.java
new file mode 100644
index 00000000..40f71f58
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/JULHandler.java
@@ -0,0 +1,169 @@
+package de.duenndns.mtmexample;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringBufferInputStream;
+import java.io.StringWriter;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import android.util.Log;
+
+/**
+ * A <code>java.util.logging</code> (JUL) Handler for Android.
+ * <p>
+ * If you want fine-grained control over MTM's logging, you can copy this
+ * class to your code base and call the static {@link #initialize()} method.
+ * </p>
+ * <p>
+ * This JUL Handler passes log messages sent to JUL to the Android log, while
+ * keeping the format and stack traces of optionally supplied Exceptions. It
+ * further allows to install a {@link DebugLogSettings} class via
+ * {@link #setDebugLogSettings(DebugLogSettings)} that determines whether JUL log messages of
+ * level {@link java.util.logging.Level#FINE} or lower are logged. This gives
+ * the application developer more control over the logged messages, while
+ * allowing a library developer to place debug log messages without risking to
+ * spam the Android log.
+ * </p>
+ * <p>
+ * If there are no {@code DebugLogSettings} configured, then all messages sent
+ * to JUL will be logged.
+ * </p>
+ *
+ * @author Florian Schmaus
+ *
+ */
+@SuppressWarnings("deprecation")
+public class JULHandler extends Handler {
+
+ /** Implement this interface to toggle debug logging.
+ */
+ public interface DebugLogSettings {
+ public boolean isDebugLogEnabled();
+ }
+
+ private static final String CLASS_NAME = JULHandler.class.getName();
+
+ /**
+ * The global LogManager configuration.
+ * <p>
+ * This configures:
+ * <ul>
+ * <li> JULHandler as the default handler for all log messages
+ * <li> A default log level FINEST (300). Meaning that log messages of a level 300 or higher a
+ * logged
+ * </ul>
+ * </p>
+ */
+ private static final InputStream LOG_MANAGER_CONFIG = new StringBufferInputStream(
+// @formatter:off
+"handlers = " + CLASS_NAME + '\n' +
+".level = FINEST"
+);
+// @formatter:on
+
+ // Constants for Android vs. JUL debug level comparisons
+ private static final int FINE_INT = Level.FINE.intValue();
+ private static final int INFO_INT = Level.INFO.intValue();
+ private static final int WARN_INT = Level.WARNING.intValue();
+ private static final int SEVE_INT = Level.SEVERE.intValue();
+
+ private static final Logger LOGGER = Logger.getLogger(CLASS_NAME);
+
+ /** A formatter that creates output similar to Android's Log.x. */
+ private static final Formatter FORMATTER = new Formatter() {
+ @Override
+ public String format(LogRecord logRecord) {
+ Throwable thrown = logRecord.getThrown();
+ if (thrown != null) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw, false);
+ pw.write(logRecord.getMessage() + ' ');
+ thrown.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ } else {
+ return logRecord.getMessage();
+ }
+ }
+ };
+
+ private static DebugLogSettings sDebugLogSettings;
+ private static boolean initialized = false;
+
+ public static void initialize() {
+ try {
+ LogManager.getLogManager().readConfiguration(LOG_MANAGER_CONFIG);
+ initialized = true;
+ } catch (IOException e) {
+ Log.e("JULHandler", "Can not initialize configuration", e);
+ }
+ if (initialized) LOGGER.info("Initialzied java.util.logging logger");
+ }
+
+ public static void setDebugLogSettings(DebugLogSettings debugLogSettings) {
+ if (!isInitialized()) initialize();
+ sDebugLogSettings = debugLogSettings;
+ }
+
+ public static boolean isInitialized() {
+ return initialized;
+ }
+
+ public JULHandler() {
+ setFormatter(FORMATTER);
+ }
+
+ @Override
+ public void close() {}
+
+ @Override
+ public void flush() {}
+
+ @Override
+ public boolean isLoggable(LogRecord record) {
+ final boolean debugLog = sDebugLogSettings == null ? true : sDebugLogSettings
+ .isDebugLogEnabled();
+
+ if (record.getLevel().intValue() <= FINE_INT) {
+ return debugLog;
+ }
+ return true;
+ }
+
+ /** JUL method that forwards log records to Android's LogCat. */
+ @Override
+ public void publish(LogRecord record) {
+ if (!isLoggable(record)) return;
+
+ final int priority = getAndroidPriority(record.getLevel());
+ final String tag = substringAfterLastDot(record.getSourceClassName());
+ final String msg = getFormatter().format(record);
+
+ Log.println(priority, tag, msg);
+ }
+
+ /** Helper to convert JUL verbosity levels to Android's Log. */
+ private static int getAndroidPriority(Level level) {
+ int value = level.intValue();
+ if (value >= SEVE_INT) {
+ return Log.ERROR;
+ } else if (value >= WARN_INT) {
+ return Log.WARN;
+ } else if (value >= INFO_INT) {
+ return Log.INFO;
+ } else {
+ return Log.DEBUG;
+ }
+ }
+
+ /** Helper to extract short class names. */
+ private static String substringAfterLastDot(String s) {
+ return s.substring(s.lastIndexOf('.') + 1).trim();
+ }
+}
diff --git a/libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/MTMExample.java b/libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/MTMExample.java
new file mode 100644
index 00000000..0d16ae82
--- /dev/null
+++ b/libs/MemorizingTrustManager/example/src/de/duenndns/mtmexample/MTMExample.java
@@ -0,0 +1,143 @@
+package de.duenndns.mtmexample;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.Window;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.net.URL;
+import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.Collections;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.X509TrustManager;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+
+/**
+ * Example to demonstrate the use of MemorizingTrustManager on HTTPS
+ * sockets.
+ */
+public class MTMExample extends Activity implements OnClickListener
+{
+ MemorizingTrustManager mtm;
+
+ TextView content;
+ HostnameVerifier defaultverifier;
+ EditText urlinput;
+ String text;
+ Handler hdlr;
+
+ /** Creates the Activity and registers a MemorizingTrustManager. */
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ JULHandler.initialize();
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.mtmexample);
+
+
+ // set up gui elements
+ findViewById(R.id.connect).setOnClickListener(this);
+ content = (TextView)findViewById(R.id.content);
+ urlinput = (EditText)findViewById(R.id.url);
+
+ // register handler for background thread
+ hdlr = new Handler();
+
+ // Here, the MemorizingTrustManager is activated for HTTPS
+ try {
+ // set location of the keystore
+ MemorizingTrustManager.setKeyStoreFile("private", "sslkeys.bks");
+
+ // register MemorizingTrustManager for HTTPS
+ SSLContext sc = SSLContext.getInstance("TLS");
+ mtm = new MemorizingTrustManager(this);
+ sc.init(null, new X509TrustManager[] { mtm },
+ new java.security.SecureRandom());
+ HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ HttpsURLConnection.setDefaultHostnameVerifier(
+ mtm.wrapHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier()));
+
+ // disable redirects to reduce possible confusion
+ HttpsURLConnection.setFollowRedirects(false);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /** Updates the screen content from a background thread. */
+ void setText(final String s, final boolean progress) {
+ text = s;
+ hdlr.post(new Runnable() {
+ public void run() {
+ content.setText(s);
+ setProgressBarIndeterminateVisibility(progress);
+ }
+ });
+ }
+
+ /** Spawns a new thread connecting to the specified URL.
+ * The result of the request is displayed on the screen.
+ * @param urlString a HTTPS URL to connect to.
+ */
+ void connect(final String urlString) {
+ new Thread() {
+ public void run() {
+ try {
+ URL u = new URL(urlString);
+ HttpsURLConnection c = (HttpsURLConnection)u.openConnection();
+ c.connect();
+ setText("" + c.getResponseCode() + " "
+ + c.getResponseMessage(), false);
+ c.disconnect();
+ } catch (Exception e) {
+ setText(e.toString(), false);
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+
+ /** Reacts on the connect Button press. */
+ @Override
+ public void onClick(View view) {
+ String url = urlinput.getText().toString();
+ setText("Loading " + url, true);
+ setProgressBarIndeterminateVisibility(true);
+ connect(url);
+ }
+
+ /** React on the "Manage Certificates" button press. */
+ public void onManage(View view) {
+ final ArrayList<String> aliases = Collections.list(mtm.getCertificates());
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.select_dialog_item, aliases);
+ new AlertDialog.Builder(this).setTitle("Tap Certificate to Delete")
+ .setNegativeButton(android.R.string.cancel, null)
+ .setAdapter(adapter, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ String alias = aliases.get(which);
+ mtm.deleteCertificate(alias);
+ setText("Deleted " + alias, false);
+ } catch (KeyStoreException e) {
+ e.printStackTrace();
+ setText("Error: " + e.getLocalizedMessage(), false);
+ }
+ }
+ })
+ .create().show();
+ }
+}
diff --git a/libs/MemorizingTrustManager/libs/.android_sucks b/libs/MemorizingTrustManager/libs/.android_sucks
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/libs/MemorizingTrustManager/libs/.android_sucks
diff --git a/libs/MemorizingTrustManager/mtm-notification.png b/libs/MemorizingTrustManager/mtm-notification.png
new file mode 100644
index 00000000..d8531790
--- /dev/null
+++ b/libs/MemorizingTrustManager/mtm-notification.png
Binary files differ
diff --git a/libs/MemorizingTrustManager/mtm-screenshot.png b/libs/MemorizingTrustManager/mtm-screenshot.png
new file mode 100644
index 00000000..41204459
--- /dev/null
+++ b/libs/MemorizingTrustManager/mtm-screenshot.png
Binary files differ
diff --git a/libs/MemorizingTrustManager/mtm-servername.png b/libs/MemorizingTrustManager/mtm-servername.png
new file mode 100644
index 00000000..332b5959
--- /dev/null
+++ b/libs/MemorizingTrustManager/mtm-servername.png
Binary files differ
diff --git a/libs/MemorizingTrustManager/proguard-project.txt b/libs/MemorizingTrustManager/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/libs/MemorizingTrustManager/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libs/MemorizingTrustManager/project.properties b/libs/MemorizingTrustManager/project.properties
new file mode 100644
index 00000000..c57400d0
--- /dev/null
+++ b/libs/MemorizingTrustManager/project.properties
@@ -0,0 +1,12 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+android.library=true
+# Project target.
+target=android-19
diff --git a/libs/MemorizingTrustManager/res/values-de/strings.xml b/libs/MemorizingTrustManager/res/values-de/strings.xml
new file mode 100644
index 00000000..17682209
--- /dev/null
+++ b/libs/MemorizingTrustManager/res/values-de/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mtm_accept_cert">Unbekanntes Zertifikat akzeptieren?</string>
+ <string name="mtm_trust_anchor">Das Serverzertifikat stammt nicht von einer bekannten Ausstellungsstelle (CA).</string>
+ <string name="mtm_cert_expired">The server certificate is expired.</string>
+ <string name="mtm_accept_servername">Abweichenden Servernamen akzeptieren?</string>
+ <string name="mtm_hostname_mismatch">Der Server konnte sich nicht als \"%s\" ausweisen. Das Zertifikat gilt nur für:</string>
+
+ <string name="mtm_connect_anyway">Verbindung trotzdem aufbauen?</string>
+ <string name="mtm_cert_details">Zertifikat-Details:</string>
+
+ <string name="mtm_decision_always">Immer</string>
+ <string name="mtm_decision_once">Einmal</string>
+ <string name="mtm_decision_abort">Abbrechen</string>
+
+ <string name="mtm_notification">Zertifikatsprüfung</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/res/values-es/strings.xml b/libs/MemorizingTrustManager/res/values-es/strings.xml
new file mode 100644
index 00000000..c989db3c
--- /dev/null
+++ b/libs/MemorizingTrustManager/res/values-es/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mtm_accept_cert">¿Aceptar certicado desconocido?</string>
+ <string name="mtm_trust_anchor">El certificado del servidor no está firmado por una Autoridad Conocida (CA).</string>
+ <string name="mtm_cert_expired">The server certificate is expired.</string>
+ <string name="mtm_accept_servername">¿Aceptar discordancia en nombre del servidor?</string>
+ <string name="mtm_hostname_mismatch">El servidor no ha podido autenticarte como \"%s\". El certificado es solo válido para:</string>
+
+ <string name="mtm_connect_anyway">¿Quieres conectar de todas formas?</string>
+ <string name="mtm_cert_details">Detalle del certificado:</string>
+
+ <string name="mtm_decision_always">Siempre</string>
+ <string name="mtm_decision_once">Una vez</string>
+ <string name="mtm_decision_abort">Abortar</string>
+
+ <string name="mtm_notification">Verificación de Certificado</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/res/values-eu/strings.xml b/libs/MemorizingTrustManager/res/values-eu/strings.xml
new file mode 100644
index 00000000..97e7c32a
--- /dev/null
+++ b/libs/MemorizingTrustManager/res/values-eu/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mtm_accept_cert">Ziurtagiri ezezaguna onartu?</string>
+ <string name="mtm_trust_anchor">Zerbitzariaren ziurtagiria ez dago Ziurtagiri-emaile Autoritate ezagun batez sinatuta.</string>
+ <string name="mtm_cert_expired">Zerbitzariaren ziurtagiria iraungi da.</string>
+ <string name="mtm_accept_servername">Zerbitzariaren izeneko desadostasuna onartu?</string>
+ <string name="mtm_hostname_mismatch">Zerbitzaria ezin izan da \&quot;%s\&quot; bezala autentifikatu. Ziurtagiria soilik honetarako baliagarria da:</string>
+
+ <string name="mtm_connect_anyway">Konektatu hala ere?</string>
+ <string name="mtm_cert_details">Ziurtagiriaren xehetasunak:</string>
+
+ <string name="mtm_decision_always">Beti</string>
+ <string name="mtm_decision_once">Behin</string>
+ <string name="mtm_decision_abort">Utzi</string>
+
+ <string name="mtm_notification">Ziurtagiriaren egiaztapena</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/res/values-fi/strings.xml b/libs/MemorizingTrustManager/res/values-fi/strings.xml
new file mode 100644
index 00000000..2dfe31ac
--- /dev/null
+++ b/libs/MemorizingTrustManager/res/values-fi/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mtm_accept_cert">Hyväksytäänkö palvelimen antama tuntematon varmenne?</string>
+ <string name="mtm_trust_anchor">Palvelimen varmenne ei ole tunnetun varmentajan (CA) allekirjoittama.</string>
+ <string name="mtm_accept_servername">Sallitaanko palvelimen nimi, joka ei vastaa varmeenteessa olevaa nimeä?</string>
+ <string name="mtm_hostname_mismatch">Palvelimella ei ole varmennetta nimelle \"%s\". Varmenteen sisältämät nimet:</string>
+
+ <string name="mtm_connect_anyway">Haluatko jatkaa yhteyden muodostamista?</string>
+ <string name="mtm_cert_details">Sertifikaatin tiedot:</string>
+
+ <string name="mtm_decision_always">Aina</string>
+ <string name="mtm_decision_once">Kerran</string>
+ <string name="mtm_decision_abort">Keskeytä</string>
+
+ <string name="mtm_notification">Varmenteen tarkistus</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/res/values-fr/strings.xml b/libs/MemorizingTrustManager/res/values-fr/strings.xml
new file mode 100644
index 00000000..db27c9af
--- /dev/null
+++ b/libs/MemorizingTrustManager/res/values-fr/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mtm_accept_cert">Accept Unknown Certificate?</string>
+ <string name="mtm_trust_anchor">Le certificat du serveur n’est pas signé par une Autorité de Certification reconnue.</string>
+ <string name="mtm_accept_servername">Accept Mismatching Server Name?</string>
+ <string name="mtm_hostname_mismatch">Server could not authenticate as \"%s\". The certificate is only valid for:</string>
+
+ <string name="mtm_connect_anyway">Do you want to connect anyway?</string>
+ <string name="mtm_cert_details">Détails du certificat :</string>
+
+ <string name="mtm_decision_always">Toujours</string>
+ <string name="mtm_decision_once">Une seule fois</string>
+ <string name="mtm_decision_abort">Annuler</string>
+
+ <string name="mtm_notification">Certificate Verification</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/res/values-no/strings.xml b/libs/MemorizingTrustManager/res/values-no/strings.xml
new file mode 100644
index 00000000..8cf9614b
--- /dev/null
+++ b/libs/MemorizingTrustManager/res/values-no/strings.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mtm_accept_cert">Godta ukjent sertifikat?</string>
+ <string name="mtm_trust_anchor">Sertifikatet er ikke utstilt av en kjent utstiller (CA).</string>
+ <string name="mtm_accept_servername">Godta feil servernavn?</string>
+ <string name="mtm_hostname_mismatch">Serveren heter ikke \"%s\". Sertifikatet gjelder bare for: </string>
+
+ <string name="mtm_connect_anyway">Vil du bruke serveren likevel?</string>
+ <string name="mtm_cert_details">Sertifikatdetaljer:</string>
+
+ <string name="mtm_decision_always">Alltid</string>
+ <string name="mtm_decision_once">En gang</string>
+ <string name="mtm_decision_abort">Avbryt</string>
+
+ <string name="mtm_notification">Sertifikat-sjekk</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/res/values/strings.xml b/libs/MemorizingTrustManager/res/values/strings.xml
new file mode 100644
index 00000000..c3862889
--- /dev/null
+++ b/libs/MemorizingTrustManager/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="mtm_accept_cert">Accept Unknown Certificate?</string>
+ <string name="mtm_trust_anchor">The server certificate is not signed by a known Certificate Authority.</string>
+ <string name="mtm_cert_expired">The server certificate is expired.</string>
+ <string name="mtm_accept_servername">Accept Mismatching Server Name?</string>
+ <string name="mtm_hostname_mismatch">Server could not authenticate as \&quot;%s\&quot;. The certificate is only valid for:</string>
+
+ <string name="mtm_connect_anyway">Do you want to connect anyway?</string>
+ <string name="mtm_cert_details">Certificate details:</string>
+
+ <string name="mtm_decision_always">Always</string>
+ <string name="mtm_decision_once">Once</string>
+ <string name="mtm_decision_abort">Abort</string>
+
+ <string name="mtm_notification">Certificate Verification</string>
+</resources>
diff --git a/libs/MemorizingTrustManager/settings.gradle b/libs/MemorizingTrustManager/settings.gradle
new file mode 100644
index 00000000..ff1d046b
--- /dev/null
+++ b/libs/MemorizingTrustManager/settings.gradle
@@ -0,0 +1 @@
+include ':example'
diff --git a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MTMDecision.java b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MTMDecision.java
new file mode 100644
index 00000000..0efe6b51
--- /dev/null
+++ b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MTMDecision.java
@@ -0,0 +1,33 @@
+/* MemorizingTrustManager - a TrustManager which asks the user about invalid
+ * certificates and memorizes their decision.
+ *
+ * Copyright (c) 2010 Georg Lukas <georg@op-co.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package de.duenndns.ssl;
+
+class MTMDecision {
+ public final static int DECISION_INVALID = 0;
+ public final static int DECISION_ABORT = 1;
+ public final static int DECISION_ONCE = 2;
+ public final static int DECISION_ALWAYS = 3;
+
+ int state = DECISION_INVALID;
+}
diff --git a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingActivity.java b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingActivity.java
new file mode 100644
index 00000000..013ac29b
--- /dev/null
+++ b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingActivity.java
@@ -0,0 +1,103 @@
+/* MemorizingTrustManager - a TrustManager which asks the user about invalid
+ * certificates and memorizes their decision.
+ *
+ * Copyright (c) 2010 Georg Lukas <georg@op-co.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package de.duenndns.ssl;
+
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.*;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class MemorizingActivity extends Activity
+ implements OnClickListener,OnCancelListener {
+
+ private final static Logger LOGGER = Logger.getLogger(MemorizingActivity.class.getName());
+
+ int decisionId;
+
+ AlertDialog dialog;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ LOGGER.log(Level.FINE, "onCreate");
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Intent i = getIntent();
+ decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
+ int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert);
+ String cert = i.getStringExtra(MemorizingTrustManager.DECISION_INTENT_CERT);
+ LOGGER.log(Level.FINE, "onResume with " + i.getExtras() + " decId=" + decisionId + " data: " + i.getData());
+ dialog = new AlertDialog.Builder(this).setTitle(titleId)
+ .setMessage(cert)
+ .setPositiveButton(R.string.mtm_decision_always, this)
+ .setNeutralButton(R.string.mtm_decision_once, this)
+ .setNegativeButton(R.string.mtm_decision_abort, this)
+ .setOnCancelListener(this)
+ .create();
+ dialog.show();
+ }
+
+ @Override
+ protected void onPause() {
+ if (dialog.isShowing())
+ dialog.dismiss();
+ super.onPause();
+ }
+
+ void sendDecision(int decision) {
+ LOGGER.log(Level.FINE, "Sending decision: " + decision);
+ MemorizingTrustManager.interactResult(decisionId, decision);
+ finish();
+ }
+
+ // react on AlertDialog button press
+ public void onClick(DialogInterface dialog, int btnId) {
+ int decision;
+ dialog.dismiss();
+ switch (btnId) {
+ case DialogInterface.BUTTON_POSITIVE:
+ decision = MTMDecision.DECISION_ALWAYS;
+ break;
+ case DialogInterface.BUTTON_NEUTRAL:
+ decision = MTMDecision.DECISION_ONCE;
+ break;
+ default:
+ decision = MTMDecision.DECISION_ABORT;
+ }
+ sendDecision(decision);
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ sendDecision(MTMDecision.DECISION_ABORT);
+ }
+}
diff --git a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java
new file mode 100644
index 00000000..9032ba25
--- /dev/null
+++ b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java
@@ -0,0 +1,735 @@
+/* MemorizingTrustManager - a TrustManager which asks the user about invalid
+ * certificates and memorizes their decision.
+ *
+ * Copyright (c) 2010 Georg Lukas <georg@op-co.de>
+ *
+ * MemorizingTrustManager.java contains the actual trust manager and interface
+ * code to create a MemorizingActivity and obtain the results.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package de.duenndns.ssl;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.SparseArray;
+import android.os.Handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.cert.*;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * A X509 trust manager implementation which asks the user about invalid
+ * certificates and memorizes their decision.
+ * <p>
+ * The certificate validity is checked using the system default X509
+ * TrustManager, creating a query Dialog if the check fails.
+ * <p>
+ * <b>WARNING:</b> This only works if a dedicated thread is used for
+ * opening sockets!
+ */
+public class MemorizingTrustManager implements X509TrustManager {
+ final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
+ final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
+ final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
+ final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
+
+ private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
+ final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
+ private final static int NOTIFICATION_ID = 100509;
+
+ final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
+
+ static String KEYSTORE_DIR = "KeyStore";
+ static String KEYSTORE_FILE = "KeyStore.bks";
+
+ Context master;
+ Activity foregroundAct;
+ NotificationManager notificationManager;
+ private static int decisionId = 0;
+ private static SparseArray<MTMDecision> openDecisions = new SparseArray<MTMDecision>();
+
+ Handler masterHandler;
+ private File keyStoreFile;
+ private KeyStore appKeyStore;
+ private X509TrustManager defaultTrustManager;
+ private X509TrustManager appTrustManager;
+
+ /** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
+ *
+ * You need to supply the application context. This has to be one of:
+ * - Application
+ * - Activity
+ * - Service
+ *
+ * The context is used for file management, to display the dialog /
+ * notification and for obtaining translated strings.
+ *
+ * @param m Context for the application.
+ * @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate.
+ */
+ public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) {
+ init(m);
+ this.appTrustManager = getTrustManager(appKeyStore);
+ this.defaultTrustManager = defaultTrustManager;
+ }
+
+ /** Creates an instance of the MemorizingTrustManager class using the system X509TrustManager.
+ *
+ * You need to supply the application context. This has to be one of:
+ * - Application
+ * - Activity
+ * - Service
+ *
+ * The context is used for file management, to display the dialog /
+ * notification and for obtaining translated strings.
+ *
+ * @param m Context for the application.
+ */
+ public MemorizingTrustManager(Context m) {
+ init(m);
+ this.appTrustManager = getTrustManager(appKeyStore);
+ this.defaultTrustManager = getTrustManager(null);
+ }
+
+ void init(Context m) {
+ master = m;
+ masterHandler = new Handler(m.getMainLooper());
+ notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ Application app;
+ if (m instanceof Application) {
+ app = (Application)m;
+ } else if (m instanceof Service) {
+ app = ((Service)m).getApplication();
+ } else if (m instanceof Activity) {
+ app = ((Activity)m).getApplication();
+ } else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
+
+ File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
+ keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
+
+ appKeyStore = loadAppKeyStore();
+ }
+
+
+ /**
+ * Returns a X509TrustManager list containing a new instance of
+ * TrustManagerFactory.
+ *
+ * This function is meant for convenience only. You can use it
+ * as follows to integrate TrustManagerFactory for HTTPS sockets:
+ *
+ * <pre>
+ * SSLContext sc = SSLContext.getInstance("TLS");
+ * sc.init(null, MemorizingTrustManager.getInstanceList(this),
+ * new java.security.SecureRandom());
+ * HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ * </pre>
+ * @param c Activity or Service to show the Dialog / Notification
+ */
+ public static X509TrustManager[] getInstanceList(Context c) {
+ return new X509TrustManager[] { new MemorizingTrustManager(c) };
+ }
+
+ /**
+ * Binds an Activity to the MTM for displaying the query dialog.
+ *
+ * This is useful if your connection is run from a service that is
+ * triggered by user interaction -- in such cases the activity is
+ * visible and the user tends to ignore the service notification.
+ *
+ * You should never have a hidden activity bound to MTM! Use this
+ * function in onResume() and @see unbindDisplayActivity in onPause().
+ *
+ * @param act Activity to be bound
+ */
+ public void bindDisplayActivity(Activity act) {
+ foregroundAct = act;
+ }
+
+ /**
+ * Removes an Activity from the MTM display stack.
+ *
+ * Always call this function when the Activity added with
+ * {@link #bindDisplayActivity(Activity)} is hidden.
+ *
+ * @param act Activity to be unbound
+ */
+ public void unbindDisplayActivity(Activity act) {
+ // do not remove if it was overridden by a different activity
+ if (foregroundAct == act)
+ foregroundAct = null;
+ }
+
+ /**
+ * Changes the path for the KeyStore file.
+ *
+ * The actual filename relative to the app's directory will be
+ * <code>app_<i>dirname</i>/<i>filename</i></code>.
+ *
+ * @param dirname directory to store the KeyStore.
+ * @param filename file name for the KeyStore.
+ */
+ public static void setKeyStoreFile(String dirname, String filename) {
+ KEYSTORE_DIR = dirname;
+ KEYSTORE_FILE = filename;
+ }
+
+ /**
+ * Get a list of all certificate aliases stored in MTM.
+ *
+ * @return an {@link Enumeration} of all certificates
+ */
+ public Enumeration<String> getCertificates() {
+ try {
+ return appKeyStore.aliases();
+ } catch (KeyStoreException e) {
+ // this should never happen, however...
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Get a certificate for a given alias.
+ *
+ * @param alias the certificate's alias as returned by {@link #getCertificates()}.
+ *
+ * @return the certificate associated with the alias or <tt>null</tt> if none found.
+ */
+ public Certificate getCertificate(String alias) {
+ try {
+ return appKeyStore.getCertificate(alias);
+ } catch (KeyStoreException e) {
+ // this should never happen, however...
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Removes the given certificate from MTMs key store.
+ *
+ * <p>
+ * <b>WARNING</b>: this does not immediately invalidate the certificate. It is
+ * well possible that (a) data is transmitted over still existing connections or
+ * (b) new connections are created using TLS renegotiation, without a new cert
+ * check.
+ * </p>
+ * @param alias the certificate's alias as returned by {@link #getCertificates()}.
+ *
+ * @throws KeyStoreException if the certificate could not be deleted.
+ */
+ public void deleteCertificate(String alias) throws KeyStoreException {
+ appKeyStore.deleteEntry(alias);
+ keyStoreUpdated();
+ }
+
+ /**
+ * Creates a new hostname verifier supporting user interaction.
+ *
+ * <p>This method creates a new {@link HostnameVerifier} that is bound to
+ * the given instance of {@link MemorizingTrustManager}, and leverages an
+ * existing {@link HostnameVerifier}. The returned verifier performs the
+ * following steps, returning as soon as one of them succeeds:
+ * </p>
+ * <ol>
+ * <li>Success, if the wrapped defaultVerifier accepts the certificate.</li>
+ * <li>Success, if the server certificate is stored in the keystore under the given hostname.</li>
+ * <li>Ask the user and return accordingly.</li>
+ * <li>Failure on exception.</li>
+ * </ol>
+ *
+ * @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check
+ * @return a new hostname verifier using the MTM's key store
+ *
+ * @throws IllegalArgumentException if the defaultVerifier parameter is null
+ */
+ public HostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier) {
+ if (defaultVerifier == null)
+ throw new IllegalArgumentException("The default verifier may not be null");
+
+ return new MemorizingHostnameVerifier(defaultVerifier);
+ }
+
+ public HostnameVerifier wrapHostnameVerifierNonInteractive(final HostnameVerifier defaultVerifier) {
+ if (defaultVerifier == null)
+ throw new IllegalArgumentException("The default verifier may not be null");
+
+ return new NonInteractiveMemorizingHostnameVerifier(defaultVerifier);
+ }
+
+ X509TrustManager getTrustManager(KeyStore ks) {
+ try {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
+ tmf.init(ks);
+ for (TrustManager t : tmf.getTrustManagers()) {
+ if (t instanceof X509TrustManager) {
+ return (X509TrustManager)t;
+ }
+ }
+ } catch (Exception e) {
+ // Here, we are covering up errors. It might be more useful
+ // however to throw them out of the constructor so the
+ // embedding app knows something went wrong.
+ LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e);
+ }
+ return null;
+ }
+
+ KeyStore loadAppKeyStore() {
+ KeyStore ks;
+ try {
+ ks = KeyStore.getInstance(KeyStore.getDefaultType());
+ } catch (KeyStoreException e) {
+ LOGGER.log(Level.SEVERE, "getAppKeyStore()", e);
+ return null;
+ }
+ try {
+ ks.load(null, null);
+ ks.load(new java.io.FileInputStream(keyStoreFile), "MTM".toCharArray());
+ } catch (java.io.FileNotFoundException e) {
+ LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e);
+ }
+ return ks;
+ }
+
+ void storeCert(String alias, Certificate cert) {
+ try {
+ appKeyStore.setCertificateEntry(alias, cert);
+ } catch (KeyStoreException e) {
+ LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e);
+ return;
+ }
+ keyStoreUpdated();
+ }
+
+ void storeCert(X509Certificate cert) {
+ storeCert(cert.getSubjectDN().toString(), cert);
+ }
+
+ void keyStoreUpdated() {
+ // reload appTrustManager
+ appTrustManager = getTrustManager(appKeyStore);
+
+ // store KeyStore to file
+ java.io.FileOutputStream fos = null;
+ try {
+ fos = new java.io.FileOutputStream(keyStoreFile);
+ appKeyStore.store(fos, "MTM".toCharArray());
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
+ }
+ }
+ }
+ }
+
+ // if the certificate is stored in the app key store, it is considered "known"
+ private boolean isCertKnown(X509Certificate cert) {
+ try {
+ return appKeyStore.getCertificateAlias(cert) != null;
+ } catch (KeyStoreException e) {
+ return false;
+ }
+ }
+
+ private boolean isExpiredException(Throwable e) {
+ do {
+ if (e instanceof CertificateExpiredException)
+ return true;
+ e = e.getCause();
+ } while (e != null);
+ return false;
+ }
+
+ public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive)
+ throws CertificateException
+ {
+ LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
+ try {
+ LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
+ if (isServer)
+ appTrustManager.checkServerTrusted(chain, authType);
+ else
+ appTrustManager.checkClientTrusted(chain, authType);
+ } catch (CertificateException ae) {
+ LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
+ // if the cert is stored in our appTrustManager, we ignore expiredness
+ if (isExpiredException(ae)) {
+ LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore");
+ return;
+ }
+ if (isCertKnown(chain[0])) {
+ LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
+ return;
+ }
+ try {
+ if (defaultTrustManager == null)
+ throw ae;
+ LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
+ if (isServer)
+ defaultTrustManager.checkServerTrusted(chain, authType);
+ else
+ defaultTrustManager.checkClientTrusted(chain, authType);
+ } catch (CertificateException e) {
+ e.printStackTrace();
+ if (interactive) {
+ interactCert(chain, authType, e);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException
+ {
+ checkCertTrusted(chain, authType, false,true);
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException
+ {
+ checkCertTrusted(chain, authType, true,true);
+ }
+
+ public X509Certificate[] getAcceptedIssuers()
+ {
+ LOGGER.log(Level.FINE, "getAcceptedIssuers()");
+ return defaultTrustManager.getAcceptedIssuers();
+ }
+
+ private int createDecisionId(MTMDecision d) {
+ int myId;
+ synchronized(openDecisions) {
+ myId = decisionId;
+ openDecisions.put(myId, d);
+ decisionId += 1;
+ }
+ return myId;
+ }
+
+ private static String hexString(byte[] data) {
+ StringBuffer si = new StringBuffer();
+ for (int i = 0; i < data.length; i++) {
+ si.append(String.format("%02x", data[i]));
+ if (i < data.length - 1)
+ si.append(":");
+ }
+ return si.toString();
+ }
+
+ private static String certHash(final X509Certificate cert, String digest) {
+ try {
+ MessageDigest md = MessageDigest.getInstance(digest);
+ md.update(cert.getEncoded());
+ return hexString(md.digest());
+ } catch (java.security.cert.CertificateEncodingException e) {
+ return e.getMessage();
+ } catch (java.security.NoSuchAlgorithmException e) {
+ return e.getMessage();
+ }
+ }
+
+ private void certDetails(StringBuffer si, X509Certificate c) {
+ SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd");
+ si.append("\n");
+ si.append(c.getSubjectDN().toString());
+ si.append("\n");
+ si.append(validityDateFormater.format(c.getNotBefore()));
+ si.append(" - ");
+ si.append(validityDateFormater.format(c.getNotAfter()));
+ si.append("\nSHA-256: ");
+ si.append(certHash(c, "SHA-256"));
+ si.append("\nSHA-1: ");
+ si.append(certHash(c, "SHA-1"));
+ si.append("\nSigned by: ");
+ si.append(c.getIssuerDN().toString());
+ si.append("\n");
+ }
+
+ private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
+ Throwable e = cause;
+ LOGGER.log(Level.FINE, "certChainMessage for " + e);
+ StringBuffer si = new StringBuffer();
+ if (e.getCause() != null) {
+ e = e.getCause();
+ // HACK: there is no sane way to check if the error is a "trust anchor
+ // not found", so we use string comparison.
+ if (NO_TRUST_ANCHOR.equals(e.getMessage())) {
+ si.append(master.getString(R.string.mtm_trust_anchor));
+ } else
+ si.append(e.getLocalizedMessage());
+ si.append("\n");
+ }
+ si.append("\n");
+ si.append(master.getString(R.string.mtm_connect_anyway));
+ si.append("\n\n");
+ si.append(master.getString(R.string.mtm_cert_details));
+ for (X509Certificate c : chain) {
+ certDetails(si, c);
+ }
+ return si.toString();
+ }
+
+ private String hostNameMessage(X509Certificate cert, String hostname) {
+ StringBuffer si = new StringBuffer();
+
+ si.append(master.getString(R.string.mtm_hostname_mismatch, hostname));
+ si.append("\n\n");
+ try {
+ Collection<List<?>> sans = cert.getSubjectAlternativeNames();
+ if (sans == null) {
+ si.append(cert.getSubjectDN());
+ si.append("\n");
+ } else for (List<?> altName : sans) {
+ Object name = altName.get(1);
+ if (name instanceof String) {
+ si.append("[");
+ si.append((Integer)altName.get(0));
+ si.append("] ");
+ si.append(name);
+ si.append("\n");
+ }
+ }
+ } catch (CertificateParsingException e) {
+ e.printStackTrace();
+ si.append("<Parsing error: ");
+ si.append(e.getLocalizedMessage());
+ si.append(">\n");
+ }
+ si.append("\n");
+ si.append(master.getString(R.string.mtm_connect_anyway));
+ si.append("\n\n");
+ si.append(master.getString(R.string.mtm_cert_details));
+ certDetails(si, cert);
+ return si.toString();
+ }
+
+ // We can use Notification.Builder once MTM's minSDK is >= 11
+ @SuppressWarnings("deprecation")
+ void startActivityNotification(Intent intent, int decisionId, String certName) {
+ Notification n = new Notification(android.R.drawable.ic_lock_lock,
+ master.getString(R.string.mtm_notification),
+ System.currentTimeMillis());
+ PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
+ n.setLatestEventInfo(master.getApplicationContext(),
+ master.getString(R.string.mtm_notification),
+ certName, call);
+ n.flags |= Notification.FLAG_AUTO_CANCEL;
+
+ notificationManager.notify(NOTIFICATION_ID + decisionId, n);
+ }
+
+ /**
+ * Returns the top-most entry of the activity stack.
+ *
+ * @return the Context of the currently bound UI or the master context if none is bound
+ */
+ Context getUI() {
+ return (foregroundAct != null) ? foregroundAct : master;
+ }
+
+ int interact(final String message, final int titleId) {
+ /* prepare the MTMDecision blocker object */
+ MTMDecision choice = new MTMDecision();
+ final int myId = createDecisionId(choice);
+
+ masterHandler.post(new Runnable() {
+ public void run() {
+ Intent ni = new Intent(master, MemorizingActivity.class);
+ ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
+ ni.putExtra(DECISION_INTENT_ID, myId);
+ ni.putExtra(DECISION_INTENT_CERT, message);
+ ni.putExtra(DECISION_TITLE_ID, titleId);
+
+ // we try to directly start the activity and fall back to
+ // making a notification
+ try {
+ getUI().startActivity(ni);
+ } catch (Exception e) {
+ LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
+ startActivityNotification(ni, myId, message);
+ }
+ }
+ });
+
+ LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId);
+ try {
+ synchronized(choice) { choice.wait(); }
+ } catch (InterruptedException e) {
+ LOGGER.log(Level.FINER, "InterruptedException", e);
+ }
+ LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state);
+ return choice.state;
+ }
+
+ void interactCert(final X509Certificate[] chain, String authType, CertificateException cause)
+ throws CertificateException
+ {
+ switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) {
+ case MTMDecision.DECISION_ALWAYS:
+ storeCert(chain[0]); // only store the server cert, not the whole chain
+ case MTMDecision.DECISION_ONCE:
+ break;
+ default:
+ throw (cause);
+ }
+ }
+
+ boolean interactHostname(X509Certificate cert, String hostname)
+ {
+ switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) {
+ case MTMDecision.DECISION_ALWAYS:
+ storeCert(hostname, cert);
+ case MTMDecision.DECISION_ONCE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected static void interactResult(int decisionId, int choice) {
+ MTMDecision d;
+ synchronized(openDecisions) {
+ d = openDecisions.get(decisionId);
+ openDecisions.remove(decisionId);
+ }
+ if (d == null) {
+ LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!");
+ return;
+ }
+ synchronized(d) {
+ d.state = choice;
+ d.notify();
+ }
+ }
+
+ class MemorizingHostnameVerifier implements HostnameVerifier {
+ private HostnameVerifier defaultVerifier;
+
+ public MemorizingHostnameVerifier(HostnameVerifier wrapped) {
+ defaultVerifier = wrapped;
+ }
+
+ protected boolean verify(String hostname, SSLSession session, boolean interactive) {
+ LOGGER.log(Level.FINE, "hostname verifier for " + hostname + ", trying default verifier first");
+ // if the default verifier accepts the hostname, we are done
+ if (defaultVerifier.verify(hostname, session)) {
+ LOGGER.log(Level.FINE, "default verifier accepted " + hostname);
+ return true;
+ }
+ // otherwise, we check if the hostname is an alias for this cert in our keystore
+ try {
+ X509Certificate cert = (X509Certificate)session.getPeerCertificates()[0];
+ //Log.d(TAG, "cert: " + cert);
+ if (cert.equals(appKeyStore.getCertificate(hostname.toLowerCase(Locale.US)))) {
+ LOGGER.log(Level.FINE, "certificate for " + hostname + " is in our keystore. accepting.");
+ return true;
+ } else {
+ LOGGER.log(Level.FINE, "server " + hostname + " provided wrong certificate, asking user.");
+ if (interactive) {
+ return interactHostname(cert, hostname);
+ } else {
+ return false;
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return verify(hostname, session, true);
+ }
+ }
+
+ class NonInteractiveMemorizingHostnameVerifier extends MemorizingHostnameVerifier {
+
+ public NonInteractiveMemorizingHostnameVerifier(HostnameVerifier wrapped) {
+ super(wrapped);
+ }
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return verify(hostname, session, true);
+ }
+
+
+ }
+
+ public X509TrustManager getNonInteractive() {
+ return new NonInteractiveMemorizingTrustManager();
+ }
+
+ private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return MemorizingTrustManager.this.getAcceptedIssuers();
+ }
+
+ }
+}
diff --git a/libs/minidns b/libs/minidns
new file mode 160000
+Subproject 9e42bff01440c1351946a432126d5a1b87fb7c7
diff --git a/libs/openpgp-api-lib b/libs/openpgp-api-lib
new file mode 160000
+Subproject 0be263d5d3effd2df5f976fa4a127017268749c
diff --git a/proguard-rules.txt b/proguard-rules.txt
new file mode 100644
index 00000000..f39d07c5
--- /dev/null
+++ b/proguard-rules.txt
@@ -0,0 +1,27 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/sam/android-sdk-linux/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+-dontwarn javax.naming.**
+
+-keep class * extends java.util.ListResourceBundle {
+ protected Object[][] getContents();
+}
+
+-keepnames class * implements android.os.Parcelable {
+ public static final ** CREATOR;
+}
diff --git a/screenshots.png b/screenshots.png
new file mode 100644
index 00000000..49367161
--- /dev/null
+++ b/screenshots.png
Binary files differ
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..45b0e9e0
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,5 @@
+include ':libs/MemorizingTrustManager'
+include ':libs/minidns'
+include ':libs/openpgp-api-lib'
+
+rootProject.name = 'Conversations'
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..7bde645f
--- /dev/null
+++ b/src/main/AndroidManifest.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="eu.siacs.conversations">
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.READ_PROFILE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.VIBRATE" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ tools:replace="android:label"
+ android:theme="@style/ConversationsTheme" >
+ <service android:name="eu.siacs.conversations.services.XmppConnectionService" />
+
+ <receiver android:name="eu.siacs.conversations.services.EventReceiver" >
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+ <action android:name="android.intent.action.ACTION_SHUTDOWN" />
+ </intent-filter>
+ </receiver>
+
+ <activity
+ android:name="eu.siacs.conversations.ui.ConversationActivity"
+ android:label="@string/title_activity_conversations"
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="stateHidden" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.StartConversationActivity"
+ android:configChanges="orientation|screenSize"
+ android:label="@string/title_activity_start_conversation"
+ android:logo="@drawable/ic_activity" >
+ <intent-filter>
+ <action android:name="android.intent.action.SENDTO" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <data android:scheme="imto" />
+ <data android:host="jabber" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data android:scheme="xmpp" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.SettingsActivity"
+ android:label="@string/title_activity_settings" >
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.ChooseContactActivity"
+ android:label="@string/title_activity_choose_contact" >
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.ManageAccountActivity"
+ android:configChanges="orientation|screenSize"
+ android:label="@string/title_activity_manage_accounts" >
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.EditAccountActivity"
+ android:windowSoftInputMode="stateHidden|adjustResize" >
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.ConferenceDetailsActivity"
+ android:label="@string/title_activity_conference_details"
+ android:windowSoftInputMode="stateHidden" >
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.ContactDetailsActivity"
+ android:label="@string/title_activity_contact_details"
+ android:windowSoftInputMode="stateHidden" >
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.PublishProfilePictureActivity"
+ android:label="@string/mgmt_account_publish_avatar"
+ android:windowSoftInputMode="stateHidden" >
+ </activity>
+ <activity
+ android:name="eu.siacs.conversations.ui.ShareWithActivity"
+ android:label="@string/title_activity_conversations" >
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <data android:mimeType="text/plain" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <data android:mimeType="image/*" />
+ </intent-filter>
+ </activity>
+ <activity android:name="de.duenndns.ssl.MemorizingActivity" />
+ </application>
+
+</manifest>
diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java
deleted file mode 100644
index 827aa772..00000000
--- a/src/main/java/de/measite/minidns/Client.java
+++ /dev/null
@@ -1,323 +0,0 @@
-package de.measite.minidns;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.LineNumberReader;
-import java.lang.reflect.Method;
-import java.net.DatagramPacket;
-import java.net.DatagramSocket;
-import java.net.InetAddress;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Random;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import de.measite.minidns.Record.CLASS;
-import de.measite.minidns.Record.TYPE;
-
-/**
- * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
- * This circumvents the missing javax.naming package on android.
- */
-public class Client {
-
- private static final Logger LOGGER = Logger.getLogger(Client.class.getName());
-
- /**
- * The internal random class for sequence generation.
- */
- protected Random random;
-
- /**
- * The buffer size for dns replies.
- */
- protected int bufferSize = 1500;
-
- /**
- * DNS timeout.
- */
- protected int timeout = 5000;
-
- /**
- * The internal DNS cache.
- */
- protected DNSCache cache;
-
- /**
- * Create a new DNS client with the given DNS cache.
- * @param cache The backend DNS cache.
- */
- public Client(DNSCache cache) {
- try {
- random = SecureRandom.getInstance("SHA1PRNG");
- } catch (NoSuchAlgorithmException e1) {
- random = new SecureRandom();
- }
- this.cache = cache;
- }
-
- /**
- * Create a new DNS client.
- */
- public Client() {
- this(null);
- }
-
- /**
- * Query a nameserver for a single entry.
- * @param name The DNS name to request.
- * @param type The DNS type to request (SRV, A, AAAA, ...).
- * @param clazz The class of the request (usually IN for Internet).
- * @param host The DNS server host.
- * @param port The DNS server port.
- * @return The response (or null on timeout / failure).
- * @throws IOException On IO Errors.
- */
- public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port)
- throws IOException
- {
- Question q = new Question(name, type, clazz);
- return query(q, host, port);
- }
-
- /**
- * Query a nameserver for a single entry.
- * @param name The DNS name to request.
- * @param type The DNS type to request (SRV, A, AAAA, ...).
- * @param clazz The class of the request (usually IN for Internet).
- * @param host The DNS server host.
- * @return The response (or null on timeout / failure).
- * @throws IOException On IO Errors.
- */
- public DNSMessage query(String name, TYPE type, CLASS clazz, String host)
- throws IOException
- {
- Question q = new Question(name, type, clazz);
- return query(q, host);
- }
-
- /**
- * Query the system nameserver for a single entry.
- * @param name The DNS name to request.
- * @param type The DNS type to request (SRV, A, AAAA, ...).
- * @param clazz The class of the request (usually IN for Internet).
- * @return The response (or null on timeout/error).
- * @return The DNSMessage reply or null.
- */
- public DNSMessage query(String name, TYPE type, CLASS clazz)
- {
- Question q = new Question(name, type, clazz);
- return query(q);
- }
-
- /**
- * Query a specific server for one entry.
- * @param q The question section of the DNS query.
- * @param host The dns server host.
- * @return The response (or null on timeout/error).
- * @throws IOException On IOErrors.
- */
- public DNSMessage query(Question q, String host) throws IOException {
- return query(q, host, 53);
- }
-
- /**
- * Query a specific server for one entry.
- * @param q The question section of the DNS query.
- * @param host The dns server host.
- * @param port the dns port.
- * @return The response (or null on timeout/error).
- * @throws IOException On IOErrors.
- */
- public DNSMessage query(Question q, String host, int port) throws IOException {
- DNSMessage dnsMessage = (cache == null) ? null : cache.get(q);
- if (dnsMessage != null) {
- return dnsMessage;
- }
- DNSMessage message = new DNSMessage();
- message.setQuestions(new Question[]{q});
- message.setRecursionDesired(true);
- message.setId(random.nextInt());
- byte[] buf = message.toArray();
- try (DatagramSocket socket = new DatagramSocket()) {
- DatagramPacket packet = new DatagramPacket(buf, buf.length,
- InetAddress.getByName(host), port);
- socket.setSoTimeout(timeout);
- socket.send(packet);
- packet = new DatagramPacket(new byte[bufferSize], bufferSize);
- socket.receive(packet);
- dnsMessage = DNSMessage.parse(packet.getData());
- if (dnsMessage.getId() != message.getId()) {
- return null;
- }
- for (Record record : dnsMessage.getAnswers()) {
- if (record.isAnswer(q)) {
- if (cache != null) {
- cache.put(q, dnsMessage);
- }
- break;
- }
- }
- return dnsMessage;
- }
- }
-
- /**
- * Query the system DNS server for one entry.
- * @param q The question section of the DNS query.
- * @return The response (or null on timeout/error).
- */
- public DNSMessage query(Question q) {
- // While this query method does in fact re-use query(Question, String)
- // we still do a cache lookup here in order to avoid unnecessary
- // findDNS()calls, which are expensive on Android. Note that we do not
- // put the results back into the Cache, as this is already done by
- // query(Question, String).
- DNSMessage message = cache.get(q);
- if (message != null) {
- return message;
- }
- String dnsServer[] = findDNS();
- for (String dns : dnsServer) {
- try {
- message = query(q, dns);
- if (message == null) {
- continue;
- }
- if (message.getResponseCode() !=
- DNSMessage.RESPONSE_CODE.NO_ERROR) {
- continue;
- }
- for (Record record: message.getAnswers()) {
- if (record.isAnswer(q)) {
- return message;
- }
- }
- } catch (IOException ioe) {
- LOGGER.log(Level.FINE, "IOException in query", ioe);
- }
- }
- return null;
- }
-
- /**
- * Retrieve a list of currently configured DNS servers.
- * @return The server array.
- */
- public String[] findDNS() {
- String[] result = findDNSByReflection();
- if (result != null) {
- LOGGER.fine("Got DNS servers via reflection: " + Arrays.toString(result));
- return result;
- }
-
- result = findDNSByExec();
- if (result != null) {
- LOGGER.fine("Got DNS servers via exec: " + Arrays.toString(result));
- return result;
- }
-
- // fallback for ipv4 and ipv6 connectivity
- // see https://developers.google.com/speed/public-dns/docs/using
- LOGGER.fine("No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]");
-
- return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"};
- }
-
- /**
- * Try to retrieve the list of dns server by executing getprop.
- * @return Array of servers, or null on failure.
- */
- protected String[] findDNSByExec() {
- try {
- Process process = Runtime.getRuntime().exec("getprop");
- InputStream inputStream = process.getInputStream();
- LineNumberReader lnr = new LineNumberReader(
- new InputStreamReader(inputStream));
- String line = null;
- HashSet<String> server = new HashSet<String>(6);
- while ((line = lnr.readLine()) != null) {
- int split = line.indexOf("]: [");
- if (split == -1) {
- continue;
- }
- String property = line.substring(1, split);
- String value = line.substring(split + 4, line.length() - 1);
- if (property.endsWith(".dns") || property.endsWith(".dns1") ||
- property.endsWith(".dns2") || property.endsWith(".dns3") ||
- property.endsWith(".dns4")) {
-
- // normalize the address
-
- InetAddress ip = InetAddress.getByName(value);
-
- if (ip == null) continue;
-
- value = ip.getHostAddress();
-
- if (value == null) continue;
- if (value.length() == 0) continue;
-
- server.add(value);
- }
- }
- if (server.size() > 0) {
- return server.toArray(new String[server.size()]);
- }
- } catch (IOException e) {
- LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e);
- }
- return null;
- }
-
- /**
- * Try to retrieve the list of dns server by calling SystemProperties.
- * @return Array of servers, or null on failure.
- */
- protected String[] findDNSByReflection() {
- try {
- Class<?> SystemProperties =
- Class.forName("android.os.SystemProperties");
- Method method = SystemProperties.getMethod("get",
- new Class[] { String.class });
-
- ArrayList<String> servers = new ArrayList<String>(5);
-
- for (String propKey : new String[] {
- "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) {
-
- String value = (String)method.invoke(null, propKey);
-
- if (value == null) continue;
- if (value.length() == 0) continue;
- if (servers.contains(value)) continue;
-
- InetAddress ip = InetAddress.getByName(value);
-
- if (ip == null) continue;
-
- value = ip.getHostAddress();
-
- if (value == null) continue;
- if (value.length() == 0) continue;
- if (servers.contains(value)) continue;
-
- servers.add(value);
- }
-
- if (servers.size() > 0) {
- return servers.toArray(new String[servers.size()]);
- }
- } catch (Exception e) {
- // we might trigger some problems this way
- LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e);
- }
- return null;
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/DNSCache.java b/src/main/java/de/measite/minidns/DNSCache.java
deleted file mode 100644
index 14a3a776..00000000
--- a/src/main/java/de/measite/minidns/DNSCache.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package de.measite.minidns;
-
-/**
- * Cache for DNS Entries. Implementations must be thread safe.
- */
-public interface DNSCache {
-
- /**
- * Add an an dns answer/response for a given dns question. Implementations
- * should honor the ttl / receive timestamp.
- * @param q The question.
- * @param message The dns message.
- */
- void put(Question q, DNSMessage message);
-
- /**
- * Request a cached dns response.
- * @param q The dns question.
- * @return The dns message.
- */
- DNSMessage get(Question q);
-
-}
diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java
deleted file mode 100644
index ab2535ce..00000000
--- a/src/main/java/de/measite/minidns/DNSMessage.java
+++ /dev/null
@@ -1,524 +0,0 @@
-package de.measite.minidns;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.Arrays;
-
-/**
- * A DNS message as defined by rfc1035. The message consists of a header and
- * 4 sections: question, answer, nameserver and addition resource record
- * section.
- * A message can either be parsed ({@link DNSMessage#parse(byte[])}) or serialized
- * ({@link DNSMessage#toArray()}).
- */
-public class DNSMessage {
-
- /**
- * Possible DNS reply codes.
- */
- public static enum RESPONSE_CODE {
- NO_ERROR(0), FORMAT_ERR(1), SERVER_FAIL(2), NX_DOMAIN(3),
- NO_IMP(4), REFUSED(5), YXDOMAIN(6), YXRRSET(7),
- NXRRSET(8), NOT_AUTH(9),NOT_ZONE(10);
-
- /**
- * Reverse lookup table for response codes.
- */
- private final static RESPONSE_CODE INVERSE_LUT[] = new RESPONSE_CODE[]{
- NO_ERROR, FORMAT_ERR, SERVER_FAIL, NX_DOMAIN, NO_IMP,
- REFUSED, YXDOMAIN, YXRRSET, NXRRSET, NOT_AUTH, NOT_ZONE,
- null, null, null, null, null
- };
-
- /**
- * The response code value.
- */
- private final byte value;
-
- /**
- * Create a new response code.
- * @param value The response code value.
- */
- private RESPONSE_CODE(int value) {
- this.value = (byte)value;
- }
-
- /**
- * Retrieve the byte value of the response code.
- * @return the response code.
- */
- public byte getValue() {
- return (byte) value;
- }
-
- /**
- * Retrieve the response code for a byte value.
- * @param value The byte value.
- * @return The symbolic response code or null.
- * @throws IllegalArgumentException if the value is not in the range of
- * 0..15.
- */
- public static RESPONSE_CODE getResponseCode(int value) {
- if (value < 0 || value > 15) {
- throw new IllegalArgumentException();
- }
- return INVERSE_LUT[value];
- }
-
- };
-
- /**
- * Symbolic DNS Opcode values.
- */
- public static enum OPCODE {
- QUERY(0),
- INVERSE_QUERY(1),
- STATUS(2),
- NOTIFY(4),
- UPDATE(5);
-
- /**
- * Lookup table for for obcode reolution.
- */
- private final static OPCODE INVERSE_LUT[] = new OPCODE[]{
- QUERY, INVERSE_QUERY, STATUS, null, NOTIFY, UPDATE, null,
- null, null, null, null, null, null, null, null
- };
-
- /**
- * The value of this opcode.
- */
- private final byte value;
-
- /**
- * Create a new opcode for a given byte value.
- * @param value The byte value of the opcode.
- */
- private OPCODE(int value) {
- this.value = (byte)value;
- }
-
- /**
- * Retrieve the byte value of this opcode.
- * @return The byte value of this opcode.
- */
- public byte getValue() {
- return value;
- }
-
- /**
- * Retrieve the symbolic name of an opcode byte.
- * @param value The byte value of the opcode.
- * @return The symbolic opcode or null.
- * @throws IllegalArgumentException If the byte value is not in the
- * range 0..15.
- */
- public static OPCODE getOpcode(int value) {
- if (value < 0 || value > 15) {
- throw new IllegalArgumentException();
- }
- return INVERSE_LUT[value];
- }
-
- };
-
- /**
- * The DNS message id.
- */
- protected int id;
-
- /**
- * The DNS message opcode.
- */
- protected OPCODE opcode;
-
- /**
- * The response code of this dns message.
- */
- protected RESPONSE_CODE responseCode;
-
- /**
- * True if this is a query.
- */
- protected boolean query;
-
- /**
- * True if this is a authorative response.
- */
- protected boolean authoritativeAnswer;
-
- /**
- * True on truncate, tcp should be used.
- */
- protected boolean truncated;
-
- /**
- * True if the server should recurse.
- */
- protected boolean recursionDesired;
-
- /**
- * True if recursion is possible.
- */
- protected boolean recursionAvailable;
-
- /**
- * True if the server regarded the response as authentic.
- */
- protected boolean authenticData;
-
- /**
- * True if the server should not check the replies.
- */
- protected boolean checkDisabled;
-
- /**
- * The question section content.
- */
- protected Question questions[];
-
- /**
- * The answers section content.
- */
- protected Record answers[];
-
- /**
- * The nameserver records.
- */
- protected Record nameserverRecords[];
-
- /**
- * Additional resousrce records.
- */
- protected Record additionalResourceRecords[];
-
- /**
- * The receive timestamp of this message.
- */
- protected long receiveTimestamp;
-
- /**
- * Retrieve the current DNS message id.
- * @return The current DNS message id.
- */
- public int getId() {
- return id;
- }
-
- /**
- * Set the current DNS message id.
- * @param id The new DNS message id.
- */
- public void setId(int id) {
- this.id = id & 0xffff;
- }
-
- /**
- * Get the receive timestamp if this message was created via parse.
- * This should be used to evaluate TTLs.
- * @return The receive timestamp in milliseconds.
- */
- public long getReceiveTimestamp() {
- return receiveTimestamp;
- }
-
- /**
- * Retrieve the query type (true or false;
- * @return True if this DNS message is a query.
- */
- public boolean isQuery() {
- return query;
- }
-
- /**
- * Set the query status of this message.
- * @param query The new query status.
- */
- public void setQuery(boolean query) {
- this.query = query;
- }
-
- /**
- * True if the DNS message is an authoritative answer.
- * @return True if this an authoritative DNS message.
- */
- public boolean isAuthoritativeAnswer() {
- return authoritativeAnswer;
- }
-
- /**
- * Set the authoritative answer flag.
- * @param authoritativeAnswer Tge new authoritative answer value.
- */
- public void setAuthoritativeAnswer(boolean authoritativeAnswer) {
- this.authoritativeAnswer = authoritativeAnswer;
- }
-
- /**
- * Retrieve the truncation status of this message. True means that the
- * client should try a tcp lookup.
- * @return True if this message was truncated.
- */
- public boolean isTruncated() {
- return truncated;
- }
-
- /**
- * Set the truncation bit on this DNS message.
- * @param truncated The new truncated bit status.
- */
- public void setTruncated(boolean truncated) {
- this.truncated = truncated;
- }
-
- /**
- * Check if this message preferes recursion.
- * @return True if recursion is desired.
- */
- public boolean isRecursionDesired() {
- return recursionDesired;
- }
-
- /**
- * Set the recursion desired flag on this message.
- * @param recursionDesired The new recusrion setting.
- */
- public void setRecursionDesired(boolean recursionDesired) {
- this.recursionDesired = recursionDesired;
- }
-
- /**
- * Retrieve the recursion available flag of this DNS message.
- * @return The recursion available flag of this message.
- */
- public boolean isRecursionAvailable() {
- return recursionAvailable;
- }
-
- /**
- * Set the recursion available flog from this DNS message.
- * @param recursionAvailable The new recursion available status.
- */
- public void setRecursionAvailable(boolean recursionAvailable) {
- this.recursionAvailable = recursionAvailable;
- }
-
- /**
- * Retrieve the authentic data flag of this message.
- * @return The authentic data flag.
- */
- public boolean isAuthenticData() {
- return authenticData;
- }
-
- /**
- * Set the authentic data flag on this DNS message.
- * @param authenticData The new authentic data flag value.
- */
- public void setAuthenticData(boolean authenticData) {
- this.authenticData = authenticData;
- }
-
- /**
- * Check if checks are disabled.
- * @return The status of the CheckDisabled flag.
- */
- public boolean isCheckDisabled() {
- return checkDisabled;
- }
-
- /**
- * Change the check status of this packet.
- * @param checkDisabled The new check disabled value.
- */
- public void setCheckDisabled(boolean checkDisabled) {
- this.checkDisabled = checkDisabled;
- }
-
- /**
- * Generate a binary dns packet out of this message.
- * @return byte[] the binary representation.
- * @throws IOException Should never happen.
- */
- public byte[] toArray() throws IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
- DataOutputStream dos = new DataOutputStream(baos);
- int header = 0;
- if (query) {
- header += 1 << 15;
- }
- if (opcode != null) {
- header += opcode.getValue() << 11;
- }
- if (authoritativeAnswer) {
- header += 1 << 10;
- }
- if (truncated) {
- header += 1 << 9;
- }
- if (recursionDesired) {
- header += 1 << 8;
- }
- if (recursionAvailable) {
- header += 1 << 7;
- }
- if (authenticData) {
- header += 1 << 5;
- }
- if (checkDisabled) {
- header += 1 << 4;
- }
- if (responseCode != null) {
- header += responseCode.getValue();
- }
- dos.writeShort((short)id);
- dos.writeShort((short)header);
- if (questions == null) {
- dos.writeShort(0);
- } else {
- dos.writeShort((short)questions.length);
- }
- if (answers == null) {
- dos.writeShort(0);
- } else {
- dos.writeShort((short)answers.length);
- }
- if (nameserverRecords == null) {
- dos.writeShort(0);
- } else {
- dos.writeShort((short)nameserverRecords.length);
- }
- if (additionalResourceRecords == null) {
- dos.writeShort(0);
- } else {
- dos.writeShort((short)additionalResourceRecords.length);
- }
- for (Question question: questions) {
- dos.write(question.toByteArray());
- }
- dos.flush();
- return baos.toByteArray();
- }
-
- /**
- * Build a DNS Message based on a binary DNS message.
- * @param data The DNS message data.
- * @return Parsed DNSMessage message.
- * @throws IOException On read errors.
- */
- public static DNSMessage parse(byte data[]) throws IOException {
- ByteArrayInputStream bis = new ByteArrayInputStream(data);
- DataInputStream dis = new DataInputStream(bis);
- DNSMessage message = new DNSMessage();
- message.id = dis.readUnsignedShort();
- int header = dis.readUnsignedShort();
- message.query = ((header >> 15) & 1) == 0;
- message.opcode = OPCODE.getOpcode((header >> 11) & 0xf);
- message.authoritativeAnswer = ((header >> 10) & 1) == 1;
- message.truncated = ((header >> 9) & 1) == 1;
- message.recursionDesired = ((header >> 8) & 1) == 1;
- message.recursionAvailable = ((header >> 7) & 1) == 1;
- message.authenticData = ((header >> 5) & 1) == 1;
- message.checkDisabled = ((header >> 4) & 1) == 1;
- message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf);
- message.receiveTimestamp = System.currentTimeMillis();
- int questionCount = dis.readUnsignedShort();
- int answerCount = dis.readUnsignedShort();
- int nameserverCount = dis.readUnsignedShort();
- int additionalResourceRecordCount = dis.readUnsignedShort();
- message.questions = new Question[questionCount];
- while (questionCount-- > 0) {
- Question q = Question.parse(dis, data);
- message.questions[questionCount] = q;
- }
- message.answers = new Record[answerCount];
- while (answerCount-- > 0) {
- Record rr = new Record();
- rr.parse(dis, data);
- message.answers[answerCount] = rr;
- }
- message.nameserverRecords = new Record[nameserverCount];
- while (nameserverCount-- > 0) {
- Record rr = new Record();
- rr.parse(dis, data);
- message.nameserverRecords[nameserverCount] = rr;
- }
- message.additionalResourceRecords =
- new Record[additionalResourceRecordCount];
- while (additionalResourceRecordCount-- > 0) {
- Record rr = new Record();
- rr.parse(dis, data);
- message.additionalResourceRecords[additionalResourceRecordCount] =
- rr;
- }
- return message;
- }
-
- /**
- * Set the question part of this message.
- * @param questions The questions.
- */
- public void setQuestions(Question ... questions) {
- this.questions = questions;
- }
-
- /**
- * Retrieve the opcode of this message.
- * @return The opcode of this message.
- */
- public OPCODE getOpcode() {
- return opcode;
- }
-
- /**
- * Retrieve the response code of this message.
- * @return The response code.
- */
- public RESPONSE_CODE getResponseCode() {
- return responseCode;
- }
-
- /**
- * Retrieve the question section of this message.
- * @return The DNS question section.
- */
- public Question[] getQuestions() {
- return questions;
- }
-
- /**
- * Retrieve the answer records of this DNS message.
- * @return The answer section of this DNS message.
- */
- public Record[] getAnswers() {
- return answers;
- }
-
- /**
- * Retrieve the nameserver records of this DNS message.
- * @return The nameserver section of this DNS message.
- */
- public Record[] getNameserverRecords() {
- return nameserverRecords;
- }
-
- /**
- * Retrieve the additional resource records attached to this DNS message.
- * @return The additional resource record section of this DNS message.
- */
- public Record[] getAdditionalResourceRecords() {
- return additionalResourceRecords;
- }
-
- public String toString() {
- return "-- DNSMessage " + id + " --\n" +
- "Q" + Arrays.toString(questions) +
- "NS" + Arrays.toString(nameserverRecords) +
- "A" + Arrays.toString(answers) +
- "ARR" + Arrays.toString(additionalResourceRecords);
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/LRUCache.java b/src/main/java/de/measite/minidns/LRUCache.java
deleted file mode 100644
index 6b9bbdc1..00000000
--- a/src/main/java/de/measite/minidns/LRUCache.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package de.measite.minidns;
-
-import java.util.LinkedHashMap;
-import java.util.Map.Entry;
-
-/**
- * LRU based DNSCache backed by a LinkedHashMap.
- */
-public class LRUCache implements DNSCache {
-
- /**
- * Internal miss count.
- */
- protected long missCount = 0l;
-
- /**
- * Internal expire count (subset of misses that was caused by expire).
- */
- protected long expireCount = 0l;
-
- /**
- * Internal hit count.
- */
- protected long hitCount = 0l;
-
- /**
- * The internal capacity of the backend cache.
- */
- protected int capacity;
-
- /**
- * The upper bound of the ttl. All longer TTLs will be capped by this ttl.
- */
- protected long maxTTL;
-
- /**
- * The backend cache.
- */
- protected LinkedHashMap<Question, DNSMessage> backend;
-
- /**
- * Create a new LRUCache with given capacity and upper bound ttl.
- * @param capacity The internal capacity.
- * @param maxTTL The upper bound for any ttl.
- */
- @SuppressWarnings("serial")
- public LRUCache(final int capacity, final long maxTTL) {
- this.capacity = capacity;
- this.maxTTL = maxTTL;
- backend = new LinkedHashMap<Question,DNSMessage>(
- Math.min(capacity + (capacity + 3) / 4 + 2, 11), 0.75f, true)
- {
- @Override
- protected boolean removeEldestEntry(
- Entry<Question, DNSMessage> eldest) {
- return size() > capacity;
- }
- };
- }
-
- /**
- * Create a new LRUCache with given capacity.
- * @param capacity The capacity of this cache.
- */
- public LRUCache(final int capacity) {
- this(capacity, Long.MAX_VALUE);
- }
-
- @Override
- public synchronized void put(Question q, DNSMessage message) {
- if (message.getReceiveTimestamp() <= 0l) {
- return;
- }
- backend.put(q, message);
- }
-
- @Override
- public synchronized DNSMessage get(Question q) {
- DNSMessage message = backend.get(q);
- if (message == null) {
- missCount++;
- return null;
- }
-
- long ttl = maxTTL;
- for (Record r : message.getAnswers()) {
- ttl = Math.min(ttl, r.ttl);
- }
- for (Record r : message.getAdditionalResourceRecords()) {
- ttl = Math.min(ttl, r.ttl);
- }
- if (message.getReceiveTimestamp() + ttl > System.currentTimeMillis()) {
- missCount++;
- expireCount++;
- backend.remove(q);
- return null;
- } else {
- hitCount++;
- return message;
- }
- }
-
- /**
- * Clear all entries in this cache.
- */
- public synchronized void clear() {
- backend.clear();
- missCount = 0l;
- hitCount = 0l;
- expireCount = 0l;
- }
-
- /**
- * Get the miss count of this cache which is the number of fruitless
- * get calls since this cache was last resetted.
- * @return The number of cache misses.
- */
- public long getMissCount() {
- return missCount;
- }
-
- /**
- * The number of expires (cache hits that have had a ttl to low to be
- * retrieved).
- * @return The expire count.
- */
- public long getExpireCount() {
- return expireCount;
- }
-
- /**
- * The cache hit count (all sucessful calls to get).
- * @return The hit count.
- */
- public long getHitCount() {
- return hitCount;
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/Question.java b/src/main/java/de/measite/minidns/Question.java
deleted file mode 100644
index 3b2fa1a1..00000000
--- a/src/main/java/de/measite/minidns/Question.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package de.measite.minidns;
-
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.Arrays;
-
-import de.measite.minidns.Record.CLASS;
-import de.measite.minidns.Record.TYPE;
-import de.measite.minidns.util.NameUtil;
-
-/**
- * A DNS question (request).
- */
-public class Question {
-
- /**
- * The question string (e.g. "measite.de").
- */
- private final String name;
-
- /**
- * The question type (e.g. A).
- */
- private final TYPE type;
-
- /**
- * The question class (usually IN / internet).
- */
- private final CLASS clazz;
-
- /**
- * UnicastQueries have the highest bit of the CLASS field set to 1.
- */
- private final boolean unicastQuery;
-
- /**
- * Cache for the serialized object.
- */
- private byte[] byteArray;
-
- /**
- * Create a dns question for the given name/type/class.
- * @param name The name e.g. "measite.de".
- * @param type The type, e.g. A.
- * @param clazz The class, usually IN (internet).
- */
- public Question(String name, TYPE type, CLASS clazz, boolean unicastQuery) {
- this.name = name;
- this.type = type;
- this.clazz = clazz;
- this.unicastQuery = unicastQuery;
- }
-
- /**
- * Create a dns question for the given name/type/class.
- * @param name The name e.g. "measite.de".
- * @param type The type, e.g. A.
- * @param clazz The class, usually IN (internet).
- */
- public Question(String name, TYPE type, CLASS clazz) {
- this(name, type, clazz, false);
- }
-
- /**
- * Create a dns question for the given name/type/IN (internet class).
- * @param name The name e.g. "measite.de".
- * @param type The type, e.g. A.
- */
- public Question(String name, TYPE type) {
- this(name, type, CLASS.IN);
- }
-
- /**
- * Retrieve the type of this question.
- * @return The type.
- */
- public TYPE getType() {
- return type;
- }
-
- /**
- * Retrieve the class of this dns question (usually internet).
- * @return The class of this dns question.
- */
- public CLASS getClazz() {
- return clazz;
- }
-
- /**
- * Retrieve the name of this dns question (e.g. "measite.de").
- * @return The name of this dns question.
- */
- public String getName() {
- return name;
- }
-
- /**
- * Parse a byte array and rebuild the dns question from it.
- * @param dis The input stream.
- * @param data The plain data (for dns name references).
- * @return The parsed dns question.
- * @throws IOException On errors (read outside of packet).
- */
- public static Question parse(DataInputStream dis, byte[] data) throws IOException {
- String name = NameUtil.parse(dis, data);
- TYPE type = TYPE.getType(dis.readUnsignedShort());
- CLASS clazz = CLASS.getClass(dis.readUnsignedShort());
- return new Question (name, type, clazz);
- }
-
- /**
- * Generate a binary paket for this dns question.
- * @return The dns question.
- */
- public byte[] toByteArray() {
- if (byteArray == null) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
- DataOutputStream dos = new DataOutputStream(baos);
-
- try {
- dos.write(NameUtil.toByteArray(this.name));
- dos.writeShort(type.getValue());
- dos.writeShort(clazz.getValue() | (unicastQuery ? (1 << 15) : 0));
- dos.flush();
- } catch (IOException e) {
- // Should never happen
- throw new IllegalStateException(e);
- }
- byteArray = baos.toByteArray();
- }
- return byteArray;
- }
-
- @Override
- public int hashCode() {
- return Arrays.hashCode(toByteArray());
- }
-
- @Override
- public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
- if (!(other instanceof Question)) {
- return false;
- }
- byte t[] = toByteArray();
- byte o[] = ((Question)other).toByteArray();
- return Arrays.equals(t, o);
- }
-
- @Override
- public String toString() {
- return "Question/" + clazz + "/" + type + ": " + name;
- }
-}
diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java
deleted file mode 100644
index ab081426..00000000
--- a/src/main/java/de/measite/minidns/Record.java
+++ /dev/null
@@ -1,343 +0,0 @@
-package de.measite.minidns;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import de.measite.minidns.record.A;
-import de.measite.minidns.record.AAAA;
-import de.measite.minidns.record.CNAME;
-import de.measite.minidns.record.Data;
-import de.measite.minidns.record.NS;
-import de.measite.minidns.record.PTR;
-import de.measite.minidns.record.SRV;
-import de.measite.minidns.record.TXT;
-import de.measite.minidns.util.NameUtil;
-
-/**
- * A generic DNS record.
- */
-public class Record {
-
- private static final Logger LOGGER = Logger.getLogger(Client.class.getName());
-
- /**
- * The record type.
- * @see <a href="http://www.iana.org/assignments/dns-parameters">IANA DNS Parameters</a>
- */
- public static enum TYPE {
- A(1),
- NS(2),
- MD(3),
- MF(4),
- CNAME(5),
- SOA(6),
- MB(7),
- MG(8),
- MR(9),
- NULL(10),
- WKS(11),
- PTR(12),
- HINFO(13),
- MINFO(14),
- MX(15),
- TXT(16),
- RP(17),
- AFSDB(18),
- X25(19),
- ISDN(20),
- RT(21),
- NSAP(22),
- NSAP_PTR(23),
- SIG(24),
- KEY(25),
- PX(26),
- GPOS(27),
- AAAA(28),
- LOC(29),
- NXT(30),
- EID(31),
- NIMLOC(32),
- SRV(33),
- ATMA(34),
- NAPTR(35),
- KX(36),
- CERT(37),
- A6(38),
- DNAME(39),
- SINK(40),
- OPT(41),
- APL(42),
- DS(43),
- SSHFP(44),
- IPSECKEY(45),
- RRSIG(46),
- NSEC(47),
- DNSKEY(48),
- DHCID(49),
- NSEC3(50),
- NSEC3PARAM(51),
- HIP(55),
- NINFO(56),
- RKEY(57),
- TALINK(58),
- SPF(99),
- UINFO(100),
- UID(101),
- GID(102),
- TKEY(249),
- TSIG(250),
- IXFR(251),
- AXFR(252),
- MAILB(253),
- MAILA(254),
- ANY(255),
- TA(32768),
- DLV(32769);
-
- /**
- * The value of this DNS record type.
- */
- private final int value;
-
- /**
- * Internal lookup table to map values to types.
- */
- private final static HashMap<Integer, TYPE> INVERSE_LUT =
- new HashMap<Integer, TYPE>();
-
- /**
- * Initialize the reverse lookup table.
- */
- static {
- for(TYPE t: TYPE.values()) {
- INVERSE_LUT.put(t.getValue(), t);
- }
- }
-
- /**
- * Create a new record type.
- * @param value The binary value of this type.
- */
- private TYPE(int value) {
- this.value = value;
- }
-
- /**
- * Retrieve the binary value of this type.
- * @return The binary value.
- */
- public int getValue() {
- return value;
- }
-
- /**
- * Retrieve the symbolic type of the binary value.
- * @param value The binary type value.
- * @return The symbolic tpye.
- */
- public static TYPE getType(int value) {
- return INVERSE_LUT.get(value);
- }
- };
-
- /**
- * The symbolic class of a DNS record (usually IN for Internet).
- */
- public static enum CLASS {
- IN(1),
- CH(3),
- HS(4),
- NONE(254),
- ANY(255);
-
- /**
- * Internal reverse lookup table to map binary class values to symbolic
- * names.
- */
- private final static HashMap<Integer, CLASS> INVERSE_LUT =
- new HashMap<Integer, CLASS>();
-
- /**
- * Initialize the interal reverse lookup table.
- */
- static {
- for(CLASS c: CLASS.values()) {
- INVERSE_LUT.put(c.getValue(), c);
- }
- }
-
- /**
- * The binary value of this dns class.
- */
- private final int value;
-
- /**
- * Create a new DNS class based on a binary value.
- * @param value The binary value of this DNS class.
- */
- private CLASS(int value) {
- this.value = value;
- }
-
- /**
- * Retrieve the binary value of this DNS class.
- * @return The binary value of this DNS class.
- */
- public int getValue() {
- return value;
- }
-
- /**
- * Retrieve the symbolic DNS class for a binary class value.
- * @param value The binary DNS class value.
- * @return The symbolic class instance.
- */
- public static CLASS getClass(int value) {
- return INVERSE_LUT.get(value);
- }
-
- }
-
- /**
- * The generic name of this record.
- */
- protected String name;
-
- /**
- * The type (and payload type) of this record.
- */
- protected TYPE type;
-
- /**
- * The record class (usually CLASS.IN).
- */
- protected CLASS clazz;
-
- /**
- * The ttl of this record.
- */
- protected long ttl;
-
- /**
- * The payload object of this record.
- */
- protected Data payloadData;
-
- /**
- * MDNS defines the highest bit of the class as the unicast query bit.
- */
- protected boolean unicastQuery;
-
- /**
- * Parse a given record based on the full message data and the current
- * stream position.
- * @param dis The DataInputStream positioned at the first record byte.
- * @param data The full message data.
- * @throws IOException In case of malformed replies.
- */
- public void parse(DataInputStream dis, byte[] data) throws IOException {
- this.name = NameUtil.parse(dis, data);
- this.type = TYPE.getType(dis.readUnsignedShort());
- int clazzValue = dis.readUnsignedShort();
- this.clazz = CLASS.getClass(clazzValue & 0x7fff);
- this.unicastQuery = (clazzValue & 0x8000) > 0;
- if (this.clazz == null) {
- LOGGER.log(Level.FINE, "Unknown class " + clazzValue);
- }
- this.ttl = (((long)dis.readUnsignedShort()) << 32) +
- dis.readUnsignedShort();
- int payloadLength = dis.readUnsignedShort();
- switch (this.type) {
- case SRV:
- this.payloadData = new SRV();
- break;
- case AAAA:
- this.payloadData = new AAAA();
- break;
- case A:
- this.payloadData = new A();
- break;
- case NS:
- this.payloadData = new NS();
- break;
- case CNAME:
- this.payloadData = new CNAME();
- break;
- case PTR:
- this.payloadData = new PTR();
- break;
- case TXT:
- this.payloadData = new TXT();
- break;
- default:
- LOGGER.log(Level.FINE, "Unparsed type " + type);
- this.payloadData = null;
- for (int i = 0; i < payloadLength; i++) {
- dis.readByte();
- }
- break;
- }
- if (this.payloadData != null) {
- this.payloadData.parse(dis, data, payloadLength);
- }
- }
-
- /**
- * Retrieve a textual representation of this resource record.
- * @return String
- */
- @Override
- public String toString() {
- if (payloadData == null) {
- return "RR " + type + "/" + clazz;
- }
- return "RR " + type + "/" + clazz + ": " + payloadData.toString();
- };
-
- /**
- * Check if this record answers a given query.
- * @param q The query.
- * @return True if this record is a valid answer.
- */
- public boolean isAnswer(Question q) {
- return ((q.getType() == type) || (q.getType() == TYPE.ANY)) &&
- ((q.getClazz() == clazz) || (q.getClazz() == CLASS.ANY)) &&
- (q.getName().equals(name));
- }
-
- /**
- * See if this query/response was a unicast query (highest class bit set).
- * @return True if it is a unicast query/response record.
- */
- public boolean isUnicastQuery() {
- return unicastQuery;
- }
-
- /**
- * The generic record name, e.g. "measite.de".
- * @return The record name.
- */
- public String getName() {
- return name;
- }
-
- /**
- * The payload data, usually a subclass of data (A, AAAA, CNAME, ...).
- * @return The payload data.
- */
- public Data getPayload() {
- return payloadData;
- }
-
- /**
- * Retrieve the record ttl.
- * @return The record ttl.
- */
- public long getTtl() {
- return ttl;
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/record/A.java b/src/main/java/de/measite/minidns/record/A.java
deleted file mode 100644
index 4311c651..00000000
--- a/src/main/java/de/measite/minidns/record/A.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package de.measite.minidns.record;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import de.measite.minidns.Record.TYPE;
-
-/**
- * A record payload (ip pointer).
- */
-public class A implements Data {
-
- /**
- * Target IP.
- */
- private byte[] ip;
-
- @Override
- public TYPE getType() {
- return TYPE.A;
- }
-
- @Override
- public byte[] toByteArray() {
- return ip;
- }
-
- @Override
- public void parse(DataInputStream dis, byte[] data, int length)
- throws IOException {
- ip = new byte[4];
- dis.readFully(ip);
- }
-
- @Override
- public String toString() {
- return Integer.toString(ip[0] & 0xff) + "." +
- Integer.toString(ip[1] & 0xff) + "." +
- Integer.toString(ip[2] & 0xff) + "." +
- Integer.toString(ip[3] & 0xff);
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/record/AAAA.java b/src/main/java/de/measite/minidns/record/AAAA.java
deleted file mode 100644
index e4fd5ecf..00000000
--- a/src/main/java/de/measite/minidns/record/AAAA.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package de.measite.minidns.record;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import de.measite.minidns.Record.TYPE;
-
-/**
- * AAAA payload (an ipv6 pointer).
- */
-public class AAAA implements Data {
-
- /**
- * The ipv6 address.
- */
- private byte[] ip;
-
- @Override
- public TYPE getType() {
- return TYPE.AAAA;
- }
-
- @Override
- public byte[] toByteArray() {
- return ip;
- }
-
- @Override
- public void parse(DataInputStream dis, byte[] data, int length)
- throws IOException {
- ip = new byte[16];
- dis.readFully(ip);
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < ip.length; i += 2) {
- if (i != 0) {
- sb.append(':');
- }
- sb.append(Integer.toHexString(
- ((ip[i] & 0xff) << 8) + (ip[i + 1] & 0xff)
- ));
- }
- return sb.toString();
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/record/CNAME.java b/src/main/java/de/measite/minidns/record/CNAME.java
deleted file mode 100644
index 1ac27814..00000000
--- a/src/main/java/de/measite/minidns/record/CNAME.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.measite.minidns.record;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import de.measite.minidns.Record.TYPE;
-import de.measite.minidns.util.NameUtil;
-
-/**
- * CNAME payload (pointer to another domain / address).
- */
-public class CNAME implements Data {
-
- protected String name;
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- @Override
- public byte[] toByteArray() {
- throw new UnsupportedOperationException("Not implemented yet");
- }
-
- @Override
- public void parse(DataInputStream dis, byte[] data, int length)
- throws IOException
- {
- this.name = NameUtil.parse(dis, data);
- }
-
- @Override
- public TYPE getType() {
- return TYPE.CNAME;
- }
-
- @Override
- public String toString() {
- return "to \"" + name + "\"";
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/record/Data.java b/src/main/java/de/measite/minidns/record/Data.java
deleted file mode 100644
index 7f2db03a..00000000
--- a/src/main/java/de/measite/minidns/record/Data.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.measite.minidns.record;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import de.measite.minidns.Record.TYPE;
-
-/**
- * Generic payload class.
- */
-public interface Data {
-
- /**
- * The payload type.
- * @return The payload type.
- */
- TYPE getType();
-
- /**
- * Binary representation of this payload.
- * @return The binary representation of this payload.
- */
- byte[] toByteArray();
-
- /**
- * Parse this payload.
- * @param dis The input stream.
- * @param data The plain data (needed for name cross references).
- * @param length The payload length.
- * @throws IOException on io error (read past paket boundary).
- */
- void parse(DataInputStream dis, byte data[], int length) throws IOException;
-
-}
diff --git a/src/main/java/de/measite/minidns/record/NS.java b/src/main/java/de/measite/minidns/record/NS.java
deleted file mode 100644
index 8ac2d4c3..00000000
--- a/src/main/java/de/measite/minidns/record/NS.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.measite.minidns.record;
-
-import de.measite.minidns.Record.TYPE;
-
-/**
- * Nameserver record.
- */
-public class NS extends CNAME {
-
- @Override
- public TYPE getType() {
- return TYPE.NS;
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/record/PTR.java b/src/main/java/de/measite/minidns/record/PTR.java
deleted file mode 100644
index 6e200655..00000000
--- a/src/main/java/de/measite/minidns/record/PTR.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.measite.minidns.record;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import de.measite.minidns.Record.TYPE;
-import de.measite.minidns.util.NameUtil;
-
-/**
- * A PTR record is handled like a CNAME
- */
-public class PTR extends CNAME {
-
- @Override
- public TYPE getType() {
- return TYPE.PTR;
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/record/SRV.java b/src/main/java/de/measite/minidns/record/SRV.java
deleted file mode 100644
index 707bf3f5..00000000
--- a/src/main/java/de/measite/minidns/record/SRV.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package de.measite.minidns.record;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import de.measite.minidns.Record.TYPE;
-import de.measite.minidns.util.NameUtil;
-
-/**
- * SRV record payload (service pointer).
- */
-public class SRV implements Data {
-
- /**
- * The priority of this service.
- */
- protected int priority;
-
- /**
- * The weight of this service.
- */
- protected int weight;
-
- /**
- * The target port.
- */
- protected int port;
-
- /**
- * The target server.
- */
- protected String name;
-
- /**
- * The priority of this service. Lower values mean higher priority.
- * @return The priority.
- */
- public int getPriority() {
- return priority;
- }
-
- /**
- * Set the priority of this service entry. Lower values have higher priority.
- * @param priority The new priority.
- */
- public void setPriority(int priority) {
- this.priority = priority;
- }
-
- /**
- * The weight of this service. Services with the same priority should be
- * balanced based on weight.
- * @return The weight of this service.
- */
- public int getWeight() {
- return weight;
- }
-
- /**
- * Set the weight of this service.
- * @param weight The new weight of this service.
- */
- public void setWeight(int weight) {
- this.weight = weight;
- }
-
- /**
- * The target port of this service.
- * @return The target port of this service.
- */
- public int getPort() {
- return port;
- }
-
- /**
- * Set the target port of this service.
- * @param port The new target port.
- */
- public void setPort(int port) {
- this.port = port;
- }
-
- /**
- * The name of the target server.
- * @return The target servers name.
- */
- public String getName() {
- return name;
- }
-
- /**
- * Set the name of the target server.
- * @param name The new target servers name.
- */
- public void setName(String name) {
- this.name = name;
- }
-
- @Override
- public byte[] toByteArray() {
- throw new UnsupportedOperationException("Not implemented yet");
- }
-
- @Override
- public void parse(DataInputStream dis, byte[] data, int length)
- throws IOException
- {
- this.priority = dis.readUnsignedShort();
- this.weight = dis.readUnsignedShort();
- this.port = dis.readUnsignedShort();
- this.name = NameUtil.parse(dis, data);
- }
-
- @Override
- public String toString() {
- return "SRV " + name + ":" + port + " p:" + priority + " w:" + weight;
- }
-
- @Override
- public TYPE getType() {
- return TYPE.SRV;
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/record/TXT.java b/src/main/java/de/measite/minidns/record/TXT.java
deleted file mode 100644
index 03e73040..00000000
--- a/src/main/java/de/measite/minidns/record/TXT.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package de.measite.minidns.record;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import de.measite.minidns.Record.TYPE;
-import de.measite.minidns.util.NameUtil;
-
-/**
- * TXT record (actually a binary blob with wrappers for text content).
- */
-public class TXT implements Data {
-
- protected byte[] blob;
-
- public byte[] getBlob() {
- return blob;
- }
-
- public void setBlob(byte[] blob) {
- this.blob = blob;
- }
-
- public String getText() {
- try {
- return (new String(blob, "UTF-8")).intern();
- } catch (Exception e) {
- /* Can't happen for UTF-8 unless it's really a blob */
- return null;
- }
- }
-
- public void setText(String text) {
- try {
- this.blob = text.getBytes("UTF-8");
- } catch (Exception e) {
- /* Can't happen, UTF-8 IS supported */
- throw new RuntimeException("UTF-8 not supported", e);
- }
- }
-
- @Override
- public byte[] toByteArray() {
- throw new UnsupportedOperationException("Not implemented yet");
- }
-
- @Override
- public void parse(DataInputStream dis, byte[] data, int length)
- throws IOException
- {
- blob = new byte[length];
- dis.readFully(blob);
- }
-
- @Override
- public TYPE getType() {
- return TYPE.TXT;
- }
-
- @Override
- public String toString() {
- return "\"" + getText() + "\"";
- }
-
-}
diff --git a/src/main/java/de/measite/minidns/util/NameUtil.java b/src/main/java/de/measite/minidns/util/NameUtil.java
deleted file mode 100644
index 7ae373bc..00000000
--- a/src/main/java/de/measite/minidns/util/NameUtil.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package de.measite.minidns.util;
-
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.net.IDN;
-import java.util.HashSet;
-import java.util.Arrays;
-
-/**
- * Utilities related to internationalized domain names and dns name handling.
- */
-public class NameUtil {
-
- /**
- * Retrieve the rough binary length of a string
- * (length + 2 bytes length prefix).
- * @param name The name string.
- * @return The binary size of the string (length + 2).
- */
- public static int size(String name) {
- return name.length() + 2;
- }
-
- /**
- * Check if two internationalized domain names are equal, possibly causing
- * a serialization of both domain names.
- * @param name1 The first domain name.
- * @param name2 The second domain name.
- * @return True if both domain names are the same.
- */
- public static boolean idnEquals(String name1, String name2) {
- if (name1 == name2) return true; // catches null, null
- if (name1 == null) return false;
- if (name2 == null) return false;
- if (name1.equals(name2)) return true;
-
- try {
- return Arrays.equals(toByteArray(name1),toByteArray(name2));
- } catch (IOException e) {
- return false; // impossible
- }
- }
-
- /**
- * Serialize a domain name under IDN rules.
- * @param name The domain name.
- * @return The binary domain name representation.
- * @throws IOException Should never happen.
- */
- public static byte[] toByteArray(String name) throws IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream(64);
- DataOutputStream dos = new DataOutputStream(baos);
- for (String s: name.split("[.\u3002\uFF0E\uFF61]")) {
- byte[] buffer = IDN.toASCII(s).getBytes();
- dos.writeByte(buffer.length);
- dos.write(buffer);
- }
- dos.writeByte(0);
- dos.flush();
- return baos.toByteArray();
- }
-
- /**
- * Parse a domain name starting at the current offset and moving the input
- * stream pointer past this domain name (even if cross references occure).
- * @param dis The input stream.
- * @param data The raw data (for cross references).
- * @return The domain name string.
- * @throws IOException Should never happen.
- */
- public static String parse(DataInputStream dis, byte data[])
- throws IOException
- {
- int c = dis.readUnsignedByte();
- if ((c & 0xc0) == 0xc0) {
- c = ((c & 0x3f) << 8) + dis.readUnsignedByte();
- HashSet<Integer> jumps = new HashSet<Integer>();
- jumps.add(c);
- return parse(data, c, jumps);
- }
- if (c == 0) {
- return "";
- }
- byte b[] = new byte[c];
- dis.readFully(b);
- String s = IDN.toUnicode(new String(b));
- String t = parse(dis, data);
- if (t.length() > 0) {
- s = s + "." + t;
- }
- return s;
- }
-
- /**
- * Parse a domain name starting at the given offset.
- * @param data The raw data.
- * @param offset The offset.
- * @param jumps The list of jumps (by now).
- * @return The parsed domain name.
- * @throws IllegalStateException on cycles.
- */
- public static String parse(
- byte data[],
- int offset,
- HashSet<Integer> jumps
- ) {
- int c = data[offset] & 0xff;
- if ((c & 0xc0) == 0xc0) {
- c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff);
- if (jumps.contains(c)) {
- throw new IllegalStateException("Cyclic offsets detected.");
- }
- jumps.add(c);
- return parse(data, c, jumps);
- }
- if (c == 0) {
- return "";
- }
- String s = new String(data,offset + 1, c);
- String t = parse(data, offset + 1 + c, jumps);
- if (t.length() > 0) {
- s = s + "." + t;
- }
- return s;
- }
-
-}
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
new file mode 100644
index 00000000..1725eca6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -0,0 +1,25 @@
+package eu.siacs.conversations;
+
+import android.graphics.Bitmap;
+
+public final class Config {
+
+ public static final String LOGTAG = "conversations";
+
+ public static final int PING_MAX_INTERVAL = 300;
+ public static final int PING_MIN_INTERVAL = 30;
+ public static final int PING_TIMEOUT = 10;
+ public static final int CONNECT_TIMEOUT = 90;
+ public static final int CARBON_GRACE_PERIOD = 60;
+
+ public static final int AVATAR_SIZE = 192;
+ public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
+
+ public static final int MESSAGE_MERGE_WINDOW = 20;
+
+ public static final boolean PARSE_EMOTICONS = false;
+
+ private Config() {
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java
new file mode 100644
index 00000000..e0bd0e79
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java
@@ -0,0 +1,231 @@
+package eu.siacs.conversations.crypto;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.session.InstanceTag;
+import net.java.otr4j.session.SessionID;
+
+public class OtrEngine implements OtrEngineHost {
+
+ private Account account;
+ private OtrPolicy otrPolicy;
+ private KeyPair keyPair;
+ private XmppConnectionService mXmppConnectionService;
+
+ public OtrEngine(XmppConnectionService service, Account account) {
+ this.account = account;
+ this.otrPolicy = new OtrPolicyImpl();
+ this.otrPolicy.setAllowV1(false);
+ this.otrPolicy.setAllowV2(true);
+ this.otrPolicy.setAllowV3(true);
+ this.keyPair = loadKey(account.getKeys());
+ this.mXmppConnectionService = service;
+ }
+
+ private KeyPair loadKey(JSONObject keys) {
+ if (keys == null) {
+ return null;
+ }
+ try {
+ BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
+ BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
+ BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
+ BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
+ BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
+ KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+ DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
+ DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
+ PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
+ PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
+ return new KeyPair(publicKey, privateKey);
+ } catch (JSONException e) {
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (InvalidKeySpecException e) {
+ return null;
+ }
+ }
+
+ private void saveKey() {
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+ KeyFactory keyFactory;
+ try {
+ keyFactory = KeyFactory.getInstance("DSA");
+ DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(
+ privateKey, DSAPrivateKeySpec.class);
+ DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey,
+ DSAPublicKeySpec.class);
+ this.account.setKey("otr_x", privateKeySpec.getX().toString(16));
+ this.account.setKey("otr_g", privateKeySpec.getG().toString(16));
+ this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
+ this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
+ this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ } catch (InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void finishedSessionMessage(SessionID arg0, String arg1)
+ throws OtrException {
+
+ }
+
+ @Override
+ public String getFallbackMessage(SessionID arg0) {
+ return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
+ }
+
+ @Override
+ public byte[] getLocalFingerprintRaw(SessionID arg0) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public PublicKey getPublicKey() {
+ if (this.keyPair == null) {
+ return null;
+ }
+ return this.keyPair.getPublic();
+ }
+
+ @Override
+ public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
+ if (this.keyPair == null) {
+ KeyPairGenerator kg;
+ try {
+ kg = KeyPairGenerator.getInstance("DSA");
+ this.keyPair = kg.genKeyPair();
+ this.saveKey();
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } catch (NoSuchAlgorithmException e) {
+ Log.d(Config.LOGTAG,
+ "error generating key pair " + e.getMessage());
+ }
+ }
+ return this.keyPair;
+ }
+
+ @Override
+ public String getReplyForUnreadableMessage(SessionID arg0) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public OtrPolicy getSessionPolicy(SessionID arg0) {
+ return otrPolicy;
+ }
+
+ @Override
+ public void injectMessage(SessionID session, String body)
+ throws OtrException {
+ MessagePacket packet = new MessagePacket();
+ packet.setFrom(account.getFullJid());
+ if (session.getUserID().isEmpty()) {
+ packet.setTo(session.getAccountID());
+ } else {
+ packet.setTo(session.getAccountID() + "/" + session.getUserID());
+ }
+ packet.setBody(body);
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ packet.setType(MessagePacket.TYPE_CHAT);
+ account.getXmppConnection().sendMessagePacket(packet);
+ }
+
+ @Override
+ public void messageFromAnotherInstanceReceived(SessionID id) {
+ Log.d(Config.LOGTAG,
+ "unreadable message received from " + id.getAccountID());
+ }
+
+ @Override
+ public void multipleInstancesDetected(SessionID arg0) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void requireEncryptedMessage(SessionID arg0, String arg1)
+ throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void showError(SessionID arg0, String arg1) throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void smpAborted(SessionID arg0) throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void smpError(SessionID arg0, int arg1, boolean arg2)
+ throws OtrException {
+ throw new OtrException(new Exception("smp error"));
+ }
+
+ @Override
+ public void unencryptedMessageReceived(SessionID arg0, String arg1)
+ throws OtrException {
+ throw new OtrException(new Exception("unencrypted message received"));
+ }
+
+ @Override
+ public void unreadableMessageReceived(SessionID arg0) throws OtrException {
+ throw new OtrException(new Exception("unreadable message received"));
+ }
+
+ @Override
+ public void unverify(SessionID arg0, String arg1) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void verify(SessionID arg0, String arg1, boolean arg2) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
new file mode 100644
index 00000000..2696c7d2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
@@ -0,0 +1,385 @@
+package eu.siacs.conversations.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.UiCallback;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+public class PgpEngine {
+ private OpenPgpApi api;
+ private XmppConnectionService mXmppConnectionService;
+
+ public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
+ this.api = api;
+ this.mXmppConnectionService = service;
+ }
+
+ public void decrypt(final Message message,
+ final UiCallback<Message> callback) {
+ Log.d(Config.LOGTAG, "decrypting message " + message.getUuid());
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message
+ .getConversation().getAccount().getJid());
+ if (message.getType() == Message.TYPE_TEXT) {
+ InputStream is = new ByteArrayInputStream(message.getBody()
+ .getBytes());
+ final OutputStream os = new ByteArrayOutputStream();
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ try {
+ os.flush();
+ if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ message.setBody(os.toString());
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ callback.success(message);
+ }
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, message);
+ return;
+ }
+
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ OpenPgpError error = result
+ .getParcelableExtra(OpenPgpApi.RESULT_ERROR);
+ Log.d(Config.LOGTAG,
+ "openpgp error: " + error.getMessage());
+ callback.error(R.string.openpgp_error, message);
+ return;
+ default:
+ return;
+ }
+ }
+ });
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ try {
+ final DownloadableFile inputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, false);
+ final DownloadableFile outputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, true);
+ outputFile.createNewFile();
+ InputStream is = new FileInputStream(inputFile);
+ OutputStream os = new FileOutputStream(outputFile);
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(
+ outputFile.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ message.setBody(Long.toString(outputFile.getSize())
+ + ',' + imageWidth + ',' + imageHeight);
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ PgpEngine.this.mXmppConnectionService
+ .updateMessage(message);
+ ;
+ callback.success(message);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried(
+ (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ return;
+ default:
+ return;
+ }
+ }
+ });
+ } catch (FileNotFoundException e) {
+ callback.error(R.string.error_decrypting_file, message);
+ } catch (IOException e) {
+ callback.error(R.string.error_decrypting_file, message);
+ }
+
+ }
+ }
+
+ public void encrypt(final Message message,
+ final UiCallback<Message> callback) {
+
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_ENCRYPT);
+ if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
+ long[] keys = { message.getConversation().getContact()
+ .getPgpKeyId() };
+ params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
+ } else {
+ params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation()
+ .getMucOptions().getPgpKeyIds());
+ }
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message
+ .getConversation().getAccount().getJid());
+
+ if (message.getType() == Message.TYPE_TEXT) {
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+
+ InputStream is = new ByteArrayInputStream(message.getBody()
+ .getBytes());
+ final OutputStream os = new ByteArrayOutputStream();
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ try {
+ os.flush();
+ StringBuilder encryptedMessageBody = new StringBuilder();
+ String[] lines = os.toString().split("\n");
+ for (int i = 2; i < lines.length - 1; ++i) {
+ if (!lines[i].contains("Version")) {
+ encryptedMessageBody.append(lines[i].trim());
+ }
+ }
+ message.setEncryptedBody(encryptedMessageBody
+ .toString());
+ callback.success(message);
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, message);
+ }
+
+ break;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ break;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ break;
+ }
+ }
+ });
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ try {
+ DownloadableFile inputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, true);
+ DownloadableFile outputFile = this.mXmppConnectionService
+ .getFileBackend().getFile(message, false);
+ outputFile.createNewFile();
+ InputStream is = new FileInputStream(inputFile);
+ OutputStream os = new FileOutputStream(outputFile);
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ callback.success(message);
+ break;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried(
+ (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ break;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ break;
+ }
+ }
+ });
+ } catch (FileNotFoundException e) {
+ Log.d(Config.LOGTAG, "file not found: " + e.getMessage());
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "io exception during file encrypt");
+ }
+ }
+ }
+
+ public long fetchKeyId(Account account, String status, String signature) {
+ if ((signature == null) || (api == null)) {
+ return 0;
+ }
+ if (status == null) {
+ status = "";
+ }
+ StringBuilder pgpSig = new StringBuilder();
+ pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----");
+ pgpSig.append('\n');
+ pgpSig.append('\n');
+ pgpSig.append(status);
+ pgpSig.append('\n');
+ pgpSig.append("-----BEGIN PGP SIGNATURE-----");
+ pgpSig.append('\n');
+ pgpSig.append('\n');
+ pgpSig.append(signature.replace("\n", "").trim());
+ pgpSig.append('\n');
+ pgpSig.append("-----END PGP SIGNATURE-----");
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+ InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Intent result = api.executeApi(params, is, os);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+ OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ OpenPgpSignatureResult sigResult = result
+ .getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
+ if (sigResult != null) {
+ return sigResult.getKeyId();
+ } else {
+ return 0;
+ }
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ return 0;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ Log.d(Config.LOGTAG,
+ "openpgp error: "
+ + ((OpenPgpError) result
+ .getParcelableExtra(OpenPgpApi.RESULT_ERROR))
+ .getMessage());
+ return 0;
+ }
+ return 0;
+ }
+
+ public void generateSignature(final Account account, String status,
+ final UiCallback<Account> callback) {
+ Intent params = new Intent();
+ params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+ params.setAction(OpenPgpApi.ACTION_SIGN);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+ InputStream is = new ByteArrayInputStream(status.getBytes());
+ final OutputStream os = new ByteArrayOutputStream();
+ api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ StringBuilder signatureBuilder = new StringBuilder();
+ try {
+ os.flush();
+ String[] lines = os.toString().split("\n");
+ boolean sig = false;
+ for (String line : lines) {
+ if (sig) {
+ if (line.contains("END PGP SIGNATURE")) {
+ sig = false;
+ } else {
+ if (!line.contains("Version")) {
+ signatureBuilder.append(line.trim());
+ }
+ }
+ }
+ if (line.contains("BEGIN PGP SIGNATURE")) {
+ sig = true;
+ }
+ }
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, account);
+ return;
+ }
+ account.setKey("pgp_signature", signatureBuilder.toString());
+ callback.success(account);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ account);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, account);
+ return;
+ }
+ }
+ });
+ }
+
+ public void hasKey(final Contact contact, final UiCallback<Contact> callback) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_GET_KEY);
+ params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount()
+ .getJid());
+ api.executeApiAsync(params, null, null, new IOpenPgpCallback() {
+
+ @Override
+ public void onReturn(Intent result) {
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS:
+ callback.success(contact);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ contact);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, contact);
+ return;
+ }
+ }
+ });
+ }
+
+ public PendingIntent getIntentForKey(Contact contact) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_GET_KEY);
+ params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount()
+ .getJid());
+ Intent result = api.executeApi(params, null, null);
+ return (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+ }
+
+ public PendingIntent getIntentForKey(Account account, long pgpKeyId) {
+ Intent params = new Intent();
+ params.setAction(OpenPgpApi.ACTION_GET_KEY);
+ params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
+ params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+ Intent result = api.executeApi(params, null, null);
+ return (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java
new file mode 100644
index 00000000..92b8a729
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java
@@ -0,0 +1,21 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+
+public abstract class AbstractEntity {
+
+ public static final String UUID = "uuid";
+
+ protected String uuid;
+
+ public String getUuid() {
+ return this.uuid;
+ }
+
+ public abstract ContentValues getContentValues();
+
+ public boolean equals(AbstractEntity entity) {
+ return this.getUuid().equals(entity.getUuid());
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
new file mode 100644
index 00000000..80a9d62f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -0,0 +1,399 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.OtrEngine;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+
+public class Account extends AbstractEntity {
+
+ public static final String TABLENAME = "accounts";
+
+ public static final String USERNAME = "username";
+ public static final String SERVER = "server";
+ public static final String PASSWORD = "password";
+ public static final String OPTIONS = "options";
+ public static final String ROSTERVERSION = "rosterversion";
+ public static final String KEYS = "keys";
+ public static final String AVATAR = "avatar";
+
+ public static final int OPTION_USETLS = 0;
+ public static final int OPTION_DISABLED = 1;
+ public static final int OPTION_REGISTER = 2;
+ public static final int OPTION_USECOMPRESSION = 3;
+
+ public static final int STATUS_CONNECTING = 0;
+ public static final int STATUS_DISABLED = -2;
+ public static final int STATUS_OFFLINE = -1;
+ public static final int STATUS_ONLINE = 1;
+ public static final int STATUS_NO_INTERNET = 2;
+ public static final int STATUS_UNAUTHORIZED = 3;
+ public static final int STATUS_SERVER_NOT_FOUND = 5;
+
+ public static final int STATUS_REGISTRATION_FAILED = 7;
+ public static final int STATUS_REGISTRATION_CONFLICT = 8;
+ public static final int STATUS_REGISTRATION_SUCCESSFULL = 9;
+ public static final int STATUS_REGISTRATION_NOT_SUPPORTED = 10;
+
+ protected String username;
+ protected String server;
+ protected String password;
+ protected int options = 0;
+ protected String rosterVersion;
+ protected String resource = "mobile";
+ protected int status = -1;
+ protected JSONObject keys = new JSONObject();
+ protected String avatar;
+
+ protected boolean online = false;
+
+ private OtrEngine otrEngine = null;
+ private XmppConnection xmppConnection = null;
+ private Presences presences = new Presences();
+ private long mEndGracePeriod = 0L;
+ private String otrFingerprint;
+ private Roster roster = null;
+
+ private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
+ public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>();
+ public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>();
+
+ public Account() {
+ this.uuid = "0";
+ }
+
+ public Account(String username, String server, String password) {
+ this(java.util.UUID.randomUUID().toString(), username, server,
+ password, 0, null, "", null);
+ }
+
+ public Account(String uuid, String username, String server,
+ String password, int options, String rosterVersion, String keys,
+ String avatar) {
+ this.uuid = uuid;
+ this.username = username;
+ this.server = server;
+ this.password = password;
+ this.options = options;
+ this.rosterVersion = rosterVersion;
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (JSONException e) {
+
+ }
+ this.avatar = avatar;
+ }
+
+ public boolean isOptionSet(int option) {
+ return ((options & (1 << option)) != 0);
+ }
+
+ public void setOption(int option, boolean value) {
+ if (value) {
+ this.options |= 1 << option;
+ } else {
+ this.options &= ~(1 << option);
+ }
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getServer() {
+ return server;
+ }
+
+ public void setServer(String server) {
+ this.server = server;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getStatus() {
+ if (isOptionSet(OPTION_DISABLED)) {
+ return STATUS_DISABLED;
+ } else {
+ return this.status;
+ }
+ }
+
+ public boolean errorStatus() {
+ int s = getStatus();
+ return (s == STATUS_REGISTRATION_FAILED
+ || s == STATUS_REGISTRATION_CONFLICT
+ || s == STATUS_REGISTRATION_NOT_SUPPORTED
+ || s == STATUS_SERVER_NOT_FOUND || s == STATUS_UNAUTHORIZED);
+ }
+
+ public boolean hasErrorStatus() {
+ if (getXmppConnection() == null) {
+ return false;
+ } else {
+ return getStatus() > STATUS_NO_INTERNET
+ && (getXmppConnection().getAttempt() >= 2);
+ }
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getResource() {
+ return this.resource;
+ }
+
+ public String getJid() {
+ return username.toLowerCase(Locale.getDefault()) + "@"
+ + server.toLowerCase(Locale.getDefault());
+ }
+
+ public JSONObject getKeys() {
+ return keys;
+ }
+
+ public String getSSLFingerprint() {
+ if (keys.has("ssl_cert")) {
+ try {
+ return keys.getString("ssl_cert");
+ } catch (JSONException e) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public void setSSLCertFingerprint(String fingerprint) {
+ this.setKey("ssl_cert", fingerprint);
+ }
+
+ public boolean setKey(String keyName, String keyValue) {
+ try {
+ this.keys.put(keyName, keyValue);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(USERNAME, username);
+ values.put(SERVER, server);
+ values.put(PASSWORD, password);
+ values.put(OPTIONS, options);
+ values.put(KEYS, this.keys.toString());
+ values.put(ROSTERVERSION, rosterVersion);
+ values.put(AVATAR, avatar);
+ return values;
+ }
+
+ public static Account fromCursor(Cursor cursor) {
+ return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(USERNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVER)),
+ cursor.getString(cursor.getColumnIndex(PASSWORD)),
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(AVATAR)));
+ }
+
+ public OtrEngine getOtrEngine(XmppConnectionService context) {
+ if (otrEngine == null) {
+ otrEngine = new OtrEngine(context, this);
+ }
+ return this.otrEngine;
+ }
+
+ public XmppConnection getXmppConnection() {
+ return this.xmppConnection;
+ }
+
+ public void setXmppConnection(XmppConnection connection) {
+ this.xmppConnection = connection;
+ }
+
+ public String getFullJid() {
+ return this.getJid() + "/" + this.resource;
+ }
+
+ public String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ DSAPublicKey pubkey = (DSAPublicKey) this.otrEngine
+ .getPublicKey();
+ if (pubkey == null) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder(
+ new OtrCryptoEngineImpl().getFingerprint(pubkey));
+ builder.insert(8, " ");
+ builder.insert(17, " ");
+ builder.insert(26, " ");
+ builder.insert(35, " ");
+ this.otrFingerprint = builder.toString();
+ } catch (OtrCryptoException e) {
+
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ public String getRosterVersion() {
+ if (this.rosterVersion == null) {
+ return "";
+ } else {
+ return this.rosterVersion;
+ }
+ }
+
+ public void setRosterVersion(String version) {
+ this.rosterVersion = version;
+ }
+
+ public String getOtrFingerprint(XmppConnectionService service) {
+ this.getOtrEngine(service);
+ return this.getOtrFingerprint();
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.updatePresence(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.removePresence(resource);
+ }
+
+ public void clearPresences() {
+ this.presences = new Presences();
+ }
+
+ public int countPresences() {
+ return this.presences.size();
+ }
+
+ public String getPgpSignature() {
+ if (keys.has("pgp_signature")) {
+ try {
+ return keys.getString("pgp_signature");
+ } catch (JSONException e) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public Roster getRoster() {
+ if (this.roster == null) {
+ this.roster = new Roster(this);
+ }
+ return this.roster;
+ }
+
+ public void setBookmarks(List<Bookmark> bookmarks) {
+ this.bookmarks = bookmarks;
+ }
+
+ public List<Bookmark> getBookmarks() {
+ return this.bookmarks;
+ }
+
+ public boolean hasBookmarkFor(String conferenceJid) {
+ for (Bookmark bmark : this.bookmarks) {
+ if (bmark.getJid().equals(conferenceJid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean setAvatar(String filename) {
+ if (this.avatar != null && this.avatar.equals(filename)) {
+ return false;
+ } else {
+ this.avatar = filename;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return this.avatar;
+ }
+
+ public int getReadableStatusId() {
+ switch (getStatus()) {
+
+ case Account.STATUS_DISABLED:
+ return R.string.account_status_disabled;
+ case Account.STATUS_ONLINE:
+ return R.string.account_status_online;
+ case Account.STATUS_CONNECTING:
+ return R.string.account_status_connecting;
+ case Account.STATUS_OFFLINE:
+ return R.string.account_status_offline;
+ case Account.STATUS_UNAUTHORIZED:
+ return R.string.account_status_unauthorized;
+ case Account.STATUS_SERVER_NOT_FOUND:
+ return R.string.account_status_not_found;
+ case Account.STATUS_NO_INTERNET:
+ return R.string.account_status_no_internet;
+ case Account.STATUS_REGISTRATION_FAILED:
+ return R.string.account_status_regis_fail;
+ case Account.STATUS_REGISTRATION_CONFLICT:
+ return R.string.account_status_regis_conflict;
+ case Account.STATUS_REGISTRATION_SUCCESSFULL:
+ return R.string.account_status_regis_success;
+ case Account.STATUS_REGISTRATION_NOT_SUPPORTED:
+ return R.string.account_status_regis_not_sup;
+ default:
+ return R.string.account_status_unknown;
+ }
+ }
+
+ public void activateGracePeriod() {
+ this.mEndGracePeriod = SystemClock.elapsedRealtime()
+ + (Config.CARBON_GRACE_PERIOD * 1000);
+ }
+
+ public void deactivateGracePeriod() {
+ this.mEndGracePeriod = 0L;
+ }
+
+ public boolean inGracePeriod() {
+ return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java
new file mode 100644
index 00000000..dd9e805c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java
@@ -0,0 +1,137 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Locale;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Bookmark extends Element implements ListItem {
+
+ private Account account;
+ private Conversation mJoinedConversation;
+
+ public Bookmark(Account account, String jid) {
+ super("conference");
+ this.setAttribute("jid", jid);
+ this.account = account;
+ }
+
+ private Bookmark(Account account) {
+ super("conference");
+ this.account = account;
+ }
+
+ public static Bookmark parse(Element element, Account account) {
+ Bookmark bookmark = new Bookmark(account);
+ bookmark.setAttributes(element.getAttributes());
+ bookmark.setChildren(element.getChildren());
+ return bookmark;
+ }
+
+ public void setAutojoin(boolean autojoin) {
+ if (autojoin) {
+ this.setAttribute("autojoin", "true");
+ } else {
+ this.setAttribute("autojoin", "false");
+ }
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setNick(String nick) {
+ Element element = this.findChild("nick");
+ if (element == null) {
+ element = this.addChild("nick");
+ }
+ element.setContent(nick);
+ }
+
+ public void setPassword(String password) {
+ Element element = this.findChild("password");
+ if (element != null) {
+ element.setContent(password);
+ }
+ }
+
+ @Override
+ public int compareTo(ListItem another) {
+ return this.getDisplayName().compareToIgnoreCase(
+ another.getDisplayName());
+ }
+
+ @Override
+ public String getDisplayName() {
+ if (this.mJoinedConversation != null
+ && (this.mJoinedConversation.getMucOptions().getSubject() != null)) {
+ return this.mJoinedConversation.getMucOptions().getSubject();
+ } else if (getName() != null) {
+ return getName();
+ } else {
+ return this.getJid().split("@")[0];
+ }
+ }
+
+ @Override
+ public String getJid() {
+ String jid = this.getAttribute("jid");
+ if (jid != null) {
+ return jid.toLowerCase(Locale.US);
+ } else {
+ return null;
+ }
+ }
+
+ public String getNick() {
+ Element nick = this.findChild("nick");
+ if (nick != null) {
+ return nick.getContent();
+ } else {
+ return null;
+ }
+ }
+
+ public boolean autojoin() {
+ String autojoin = this.getAttribute("autojoin");
+ return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin
+ .equalsIgnoreCase("1")));
+ }
+
+ public String getPassword() {
+ Element password = this.findChild("password");
+ if (password != null) {
+ return password.getContent();
+ } else {
+ return null;
+ }
+ }
+
+ public boolean match(String needle) {
+ return needle == null
+ || getJid().contains(needle.toLowerCase(Locale.US))
+ || getDisplayName().toLowerCase(Locale.US).contains(
+ needle.toLowerCase(Locale.US));
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public void setConversation(Conversation conversation) {
+ this.mJoinedConversation = conversation;
+ }
+
+ public Conversation getConversation() {
+ return this.mJoinedConversation;
+ }
+
+ public String getName() {
+ return this.getAttribute("name");
+ }
+
+ public void unregisterConversation() {
+ if (this.mJoinedConversation != null) {
+ this.mJoinedConversation.deregisterWithBookmark();
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
new file mode 100644
index 00000000..60c31a42
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -0,0 +1,367 @@
+package eu.siacs.conversations.entities;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.xml.Element;
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class Contact implements ListItem {
+ public static final String TABLENAME = "contacts";
+
+ public static final String SYSTEMNAME = "systemname";
+ public static final String SERVERNAME = "servername";
+ public static final String JID = "jid";
+ public static final String OPTIONS = "options";
+ public static final String SYSTEMACCOUNT = "systemaccount";
+ public static final String PHOTOURI = "photouri";
+ public static final String KEYS = "pgpkey";
+ public static final String ACCOUNT = "accountUuid";
+ public static final String AVATAR = "avatar";
+
+ protected String accountUuid;
+ protected String systemName;
+ protected String serverName;
+ protected String presenceName;
+ protected String jid;
+ protected int subscription = 0;
+ protected String systemAccount;
+ protected String photoUri;
+ protected String avatar;
+ protected JSONObject keys = new JSONObject();
+ protected Presences presences = new Presences();
+
+ protected Account account;
+
+ protected boolean inRoster = true;
+
+ public Lastseen lastseen = new Lastseen();
+
+ public Contact(String account, String systemName, String serverName,
+ String jid, int subscription, String photoUri,
+ String systemAccount, String keys, String avatar) {
+ this.accountUuid = account;
+ this.systemName = systemName;
+ this.serverName = serverName;
+ this.jid = jid;
+ this.subscription = subscription;
+ this.photoUri = photoUri;
+ this.systemAccount = systemAccount;
+ if (keys == null) {
+ keys = "";
+ }
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (JSONException e) {
+ this.keys = new JSONObject();
+ }
+ this.avatar = avatar;
+ }
+
+ public Contact(String jid) {
+ this.jid = jid;
+ }
+
+ public String getDisplayName() {
+ if (this.systemName != null) {
+ return this.systemName;
+ } else if (this.serverName != null) {
+ return this.serverName;
+ } else if (this.presenceName != null) {
+ return this.presenceName;
+ } else {
+ return this.jid.split("@")[0];
+ }
+ }
+
+ public String getProfilePhoto() {
+ return this.photoUri;
+ }
+
+ public String getJid() {
+ return this.jid.toLowerCase(Locale.getDefault());
+ }
+
+ public boolean match(String needle) {
+ return needle == null
+ || jid.contains(needle.toLowerCase())
+ || getDisplayName().toLowerCase()
+ .contains(needle.toLowerCase());
+ }
+
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(ACCOUNT, accountUuid);
+ values.put(SYSTEMNAME, systemName);
+ values.put(SERVERNAME, serverName);
+ values.put(JID, jid);
+ values.put(OPTIONS, subscription);
+ values.put(SYSTEMACCOUNT, systemAccount);
+ values.put(PHOTOURI, photoUri);
+ values.put(KEYS, keys.toString());
+ values.put(AVATAR, avatar);
+ return values;
+ }
+
+ public static Contact fromCursor(Cursor cursor) {
+ return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVERNAME)),
+ cursor.getString(cursor.getColumnIndex(JID)),
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(PHOTOURI)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(AVATAR)));
+ }
+
+ public int getSubscription() {
+ return this.subscription;
+ }
+
+ public void setSystemAccount(String account) {
+ this.systemAccount = account;
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ this.accountUuid = account.getUuid();
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Presences getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.updatePresence(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.removePresence(resource);
+ }
+
+ public void clearPresences() {
+ this.presences.clearPresences();
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ }
+
+ public int getMostAvailableStatus() {
+ return this.presences.getMostAvailableStatus();
+ }
+
+ public void setPresences(Presences pres) {
+ this.presences = pres;
+ }
+
+ public void setPhotoUri(String uri) {
+ this.photoUri = uri;
+ }
+
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ public void setSystemName(String systemName) {
+ this.systemName = systemName;
+ }
+
+ public void setPresenceName(String presenceName) {
+ this.presenceName = presenceName;
+ }
+
+ public String getSystemAccount() {
+ return systemAccount;
+ }
+
+ public Set<String> getOtrFingerprints() {
+ Set<String> set = new HashSet<String>();
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ JSONArray fingerprints = this.keys
+ .getJSONArray("otr_fingerprints");
+ for (int i = 0; i < fingerprints.length(); ++i) {
+ set.add(fingerprints.getString(i));
+ }
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return set;
+ }
+
+ public void addOtrFingerprint(String print) {
+ try {
+ JSONArray fingerprints;
+ if (!this.keys.has("otr_fingerprints")) {
+ fingerprints = new JSONArray();
+
+ } else {
+ fingerprints = this.keys.getJSONArray("otr_fingerprints");
+ }
+ fingerprints.put(print);
+ this.keys.put("otr_fingerprints", fingerprints);
+ } catch (JSONException e) {
+
+ }
+ }
+
+ public void setPgpKeyId(long keyId) {
+ try {
+ this.keys.put("pgp_keyid", keyId);
+ } catch (JSONException e) {
+
+ }
+ }
+
+ public long getPgpKeyId() {
+ if (this.keys.has("pgp_keyid")) {
+ try {
+ return this.keys.getLong("pgp_keyid");
+ } catch (JSONException e) {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ public void setOption(int option) {
+ this.subscription |= 1 << option;
+ }
+
+ public void resetOption(int option) {
+ this.subscription &= ~(1 << option);
+ }
+
+ public boolean getOption(int option) {
+ return ((this.subscription & (1 << option)) != 0);
+ }
+
+ public boolean showInRoster() {
+ return (this.getOption(Contact.Options.IN_ROSTER) && (!this
+ .getOption(Contact.Options.DIRTY_DELETE)))
+ || (this.getOption(Contact.Options.DIRTY_PUSH));
+ }
+
+ public void parseSubscriptionFromElement(Element item) {
+ String ask = item.getAttribute("ask");
+ String subscription = item.getAttribute("subscription");
+
+ if (subscription != null) {
+ if (subscription.equals("to")) {
+ this.resetOption(Contact.Options.FROM);
+ this.setOption(Contact.Options.TO);
+ } else if (subscription.equals("from")) {
+ this.resetOption(Contact.Options.TO);
+ this.setOption(Contact.Options.FROM);
+ this.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else if (subscription.equals("both")) {
+ this.setOption(Contact.Options.TO);
+ this.setOption(Contact.Options.FROM);
+ this.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else if (subscription.equals("none")) {
+ this.resetOption(Contact.Options.FROM);
+ this.resetOption(Contact.Options.TO);
+ }
+ }
+
+ // do NOT override asking if pending push request
+ if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
+ if ((ask != null) && (ask.equals("subscribe"))) {
+ this.setOption(Contact.Options.ASKING);
+ } else {
+ this.resetOption(Contact.Options.ASKING);
+ }
+ }
+ }
+
+ public Element asElement() {
+ Element item = new Element("item");
+ item.setAttribute("jid", this.jid);
+ if (this.serverName != null) {
+ item.setAttribute("name", this.serverName);
+ }
+ return item;
+ }
+
+ public class Options {
+ public static final int TO = 0;
+ public static final int FROM = 1;
+ public static final int ASKING = 2;
+ public static final int PREEMPTIVE_GRANT = 3;
+ public static final int IN_ROSTER = 4;
+ public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
+ public static final int DIRTY_PUSH = 6;
+ public static final int DIRTY_DELETE = 7;
+ }
+
+ public class Lastseen {
+ public long time = 0;
+ public String presence = null;
+ }
+
+ @Override
+ public int compareTo(ListItem another) {
+ return this.getDisplayName().compareToIgnoreCase(
+ another.getDisplayName());
+ }
+
+ public String getServer() {
+ String[] split = getJid().split("@");
+ if (split.length >= 2) {
+ return split[1];
+ } else {
+ return null;
+ }
+ }
+
+ public boolean setAvatar(String filename) {
+ if (this.avatar != null && this.avatar.equals(filename)) {
+ return false;
+ } else {
+ this.avatar = filename;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return this.avatar;
+ }
+
+ public boolean deleteOtrFingerprint(String fingerprint) {
+ boolean success = false;
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ JSONArray newPrints = new JSONArray();
+ JSONArray oldPrints = this.keys
+ .getJSONArray("otr_fingerprints");
+ for (int i = 0; i < oldPrints.length(); ++i) {
+ if (!oldPrints.getString(i).equals(fingerprint)) {
+ newPrints.put(oldPrints.getString(i));
+ } else {
+ success = true;
+ }
+ }
+ this.keys.put("otr_fingerprints", newPrints);
+ }
+ return success;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ public boolean trusted() {
+ return getOption(Options.FROM) && getOption(Options.TO);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
new file mode 100644
index 00000000..9d4c36db
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -0,0 +1,500 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+
+public class Conversation extends AbstractEntity {
+ public static final String TABLENAME = "conversations";
+
+ public static final int STATUS_AVAILABLE = 0;
+ public static final int STATUS_ARCHIVED = 1;
+ public static final int STATUS_DELETED = 2;
+
+ public static final int MODE_MULTI = 1;
+ public static final int MODE_SINGLE = 0;
+
+ public static final String NAME = "name";
+ public static final String ACCOUNT = "accountUuid";
+ public static final String CONTACT = "contactUuid";
+ public static final String CONTACTJID = "contactJid";
+ public static final String STATUS = "status";
+ public static final String CREATED = "created";
+ public static final String MODE = "mode";
+ public static final String ATTRIBUTES = "attributes";
+
+ public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
+ public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
+ public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
+
+ private String name;
+ private String contactUuid;
+ private String accountUuid;
+ private String contactJid;
+ private int status;
+ private long created;
+ private int mode;
+
+ private JSONObject attributes = new JSONObject();
+
+ private String nextPresence;
+
+ protected ArrayList<Message> messages = new ArrayList<Message>();
+ protected Account account = null;
+
+ private transient SessionImpl otrSession;
+
+ private transient String otrFingerprint = null;
+
+ private String nextMessage;
+
+ private transient MucOptions mucOptions = null;
+
+ // private transient String latestMarkableMessageId;
+
+ private byte[] symmetricKey;
+
+ private Bookmark bookmark;
+
+ public Conversation(String name, Account account, String contactJid,
+ int mode) {
+ this(java.util.UUID.randomUUID().toString(), name, null, account
+ .getUuid(), contactJid, System.currentTimeMillis(),
+ STATUS_AVAILABLE, mode, "");
+ this.account = account;
+ }
+
+ public Conversation(String uuid, String name, String contactUuid,
+ String accountUuid, String contactJid, long created, int status,
+ int mode, String attributes) {
+ this.uuid = uuid;
+ this.name = name;
+ this.contactUuid = contactUuid;
+ this.accountUuid = accountUuid;
+ this.contactJid = contactJid;
+ this.created = created;
+ this.status = status;
+ this.mode = mode;
+ try {
+ if (attributes == null) {
+ attributes = new String();
+ }
+ this.attributes = new JSONObject(attributes);
+ } catch (JSONException e) {
+ this.attributes = new JSONObject();
+ }
+ }
+
+ public List<Message> getMessages() {
+ return messages;
+ }
+
+ public boolean isRead() {
+ if ((this.messages == null) || (this.messages.size() == 0))
+ return true;
+ return this.messages.get(this.messages.size() - 1).isRead();
+ }
+
+ public void markRead() {
+ if (this.messages == null) {
+ return;
+ }
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (messages.get(i).isRead()) {
+ break;
+ }
+ this.messages.get(i).markRead();
+ }
+ }
+
+ public String getLatestMarkableMessageId() {
+ if (this.messages == null) {
+ return null;
+ }
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
+ && this.messages.get(i).markable) {
+ if (this.messages.get(i).isRead()) {
+ return null;
+ } else {
+ return this.messages.get(i).getRemoteMsgId();
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message getLatestMessage() {
+ if ((this.messages == null) || (this.messages.size() == 0)) {
+ Message message = new Message(this, "", Message.ENCRYPTION_NONE);
+ message.setTime(getCreated());
+ return message;
+ } else {
+ Message message = this.messages.get(this.messages.size() - 1);
+ message.setConversation(this);
+ return message;
+ }
+ }
+
+ public void setMessages(ArrayList<Message> msgs) {
+ this.messages = msgs;
+ }
+
+ public String getName() {
+ if (getMode() == MODE_MULTI && getMucOptions().getSubject() != null) {
+ return getMucOptions().getSubject();
+ } else if (getMode() == MODE_MULTI && bookmark != null
+ && bookmark.getName() != null) {
+ return bookmark.getName();
+ } else {
+ return this.getContact().getDisplayName();
+ }
+ }
+
+ public String getProfilePhotoString() {
+ return this.getContact().getProfilePhoto();
+ }
+
+ public String getAccountUuid() {
+ return this.accountUuid;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Contact getContact() {
+ return this.account.getRoster().getContact(this.contactJid);
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ }
+
+ public String getContactJid() {
+ return this.contactJid;
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+
+ public long getCreated() {
+ return this.created;
+ }
+
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(NAME, name);
+ values.put(CONTACT, contactUuid);
+ values.put(ACCOUNT, accountUuid);
+ values.put(CONTACTJID, contactJid);
+ values.put(CREATED, created);
+ values.put(STATUS, status);
+ values.put(MODE, mode);
+ values.put(ATTRIBUTES, attributes.toString());
+ return values;
+ }
+
+ public static Conversation fromCursor(Cursor cursor) {
+ return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(NAME)),
+ cursor.getString(cursor.getColumnIndex(CONTACT)),
+ cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(CONTACTJID)),
+ cursor.getLong(cursor.getColumnIndex(CREATED)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(MODE)),
+ cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getMode() {
+ return this.mode;
+ }
+
+ public void setMode(int mode) {
+ this.mode = mode;
+ }
+
+ public SessionImpl startOtrSession(XmppConnectionService service,
+ String presence, boolean sendStart) {
+ if (this.otrSession != null) {
+ return this.otrSession;
+ } else {
+ SessionID sessionId = new SessionID(this.getContactJid().split("/",
+ 2)[0], presence, "xmpp");
+ this.otrSession = new SessionImpl(sessionId, getAccount()
+ .getOtrEngine(service));
+ try {
+ if (sendStart) {
+ this.otrSession.startSession();
+ return this.otrSession;
+ }
+ return this.otrSession;
+ } catch (OtrException e) {
+ return null;
+ }
+ }
+
+ }
+
+ public SessionImpl getOtrSession() {
+ return this.otrSession;
+ }
+
+ public void resetOtrSession() {
+ this.otrFingerprint = null;
+ this.otrSession = null;
+ }
+
+ public void startOtrIfNeeded() {
+ if (this.otrSession != null
+ && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
+ try {
+ this.otrSession.startSession();
+ } catch (OtrException e) {
+ this.resetOtrSession();
+ }
+ }
+ }
+
+ public boolean endOtrIfNeeded() {
+ if (this.otrSession != null) {
+ if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
+ try {
+ this.otrSession.endSession();
+ this.resetOtrSession();
+ return true;
+ } catch (OtrException e) {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public boolean hasValidOtrSession() {
+ return this.otrSession != null;
+ }
+
+ public String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ if (getOtrSession() == null) {
+ return "";
+ }
+ DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession()
+ .getRemotePublicKey();
+ StringBuilder builder = new StringBuilder(
+ new OtrCryptoEngineImpl().getFingerprint(remotePubKey));
+ builder.insert(8, " ");
+ builder.insert(17, " ");
+ builder.insert(26, " ");
+ builder.insert(35, " ");
+ this.otrFingerprint = builder.toString();
+ } catch (OtrCryptoException e) {
+
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ public synchronized MucOptions getMucOptions() {
+ if (this.mucOptions == null) {
+ this.mucOptions = new MucOptions(this);
+ }
+ return this.mucOptions;
+ }
+
+ public void resetMucOptions() {
+ this.mucOptions = null;
+ }
+
+ public void setContactJid(String jid) {
+ this.contactJid = jid;
+ }
+
+ public void setNextPresence(String presence) {
+ this.nextPresence = presence;
+ }
+
+ public String getNextPresence() {
+ return this.nextPresence;
+ }
+
+ public int getLatestEncryption() {
+ int latestEncryption = this.getLatestMessage().getEncryption();
+ if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
+ || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
+ return Message.ENCRYPTION_PGP;
+ } else {
+ return latestEncryption;
+ }
+ }
+
+ public int getNextEncryption(boolean force) {
+ int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
+ if (next == -1) {
+ int latest = this.getLatestEncryption();
+ if (latest == Message.ENCRYPTION_NONE) {
+ if (force && getMode() == MODE_SINGLE) {
+ return Message.ENCRYPTION_OTR;
+ } else if (getContact().getPresences().size() == 1) {
+ if (getContact().getOtrFingerprints().size() >= 1) {
+ return Message.ENCRYPTION_OTR;
+ } else {
+ return latest;
+ }
+ } else {
+ return latest;
+ }
+ } else {
+ return latest;
+ }
+ }
+ if (next == Message.ENCRYPTION_NONE && force
+ && getMode() == MODE_SINGLE) {
+ return Message.ENCRYPTION_OTR;
+ } else {
+ return next;
+ }
+ }
+
+ public void setNextEncryption(int encryption) {
+ this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
+ }
+
+ public String getNextMessage() {
+ if (this.nextMessage == null) {
+ return "";
+ } else {
+ return this.nextMessage;
+ }
+ }
+
+ public void setNextMessage(String message) {
+ this.nextMessage = message;
+ }
+
+ public void setSymmetricKey(byte[] key) {
+ this.symmetricKey = key;
+ }
+
+ public byte[] getSymmetricKey() {
+ return this.symmetricKey;
+ }
+
+ public void setBookmark(Bookmark bookmark) {
+ this.bookmark = bookmark;
+ this.bookmark.setConversation(this);
+ }
+
+ public void deregisterWithBookmark() {
+ if (this.bookmark != null) {
+ this.bookmark.setConversation(null);
+ }
+ }
+
+ public Bookmark getBookmark() {
+ return this.bookmark;
+ }
+
+ public boolean hasDuplicateMessage(Message message) {
+ for (int i = this.getMessages().size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).equals(message)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setMutedTill(long value) {
+ this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
+ }
+
+ public boolean isMuted() {
+ return SystemClock.elapsedRealtime() < this.getLongAttribute(
+ ATTRIBUTE_MUTED_TILL, 0);
+ }
+
+ public boolean setAttribute(String key, String value) {
+ try {
+ this.attributes.put(key, value);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ public String getAttribute(String key) {
+ try {
+ return this.attributes.getString(key);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public int getIntAttribute(String key, int defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ }
+
+ public long getLongAttribute(String key, long defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ }
+
+ public void add(Message message) {
+ message.setConversation(this);
+ synchronized (this.messages) {
+ this.messages.add(message);
+ }
+ }
+
+ public void addAll(int index, List<Message> messages) {
+ synchronized (this.messages) {
+ this.messages.addAll(index, messages);
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Downloadable.java b/src/main/java/eu/siacs/conversations/entities/Downloadable.java
new file mode 100644
index 00000000..70516b20
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Downloadable.java
@@ -0,0 +1,21 @@
+package eu.siacs.conversations.entities;
+
+public interface Downloadable {
+
+ public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" };
+ public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" };
+
+ public static final int STATUS_UNKNOWN = 0x200;
+ public static final int STATUS_CHECKING = 0x201;
+ public static final int STATUS_FAILED = 0x202;
+ public static final int STATUS_OFFER = 0x203;
+ public static final int STATUS_DOWNLOADING = 0x204;
+ public static final int STATUS_DELETED = 0x205;
+ public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+
+ public boolean start();
+
+ public int getStatus();
+
+ public long getFileSize();
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
new file mode 100644
index 00000000..1605c75b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
@@ -0,0 +1,154 @@
+package eu.siacs.conversations.entities;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
+import android.util.Log;
+
+public class DownloadableFile extends File {
+
+ private static final long serialVersionUID = 2247012619505115863L;
+
+ private long expectedSize = 0;
+ private String sha1sum;
+ private Key aeskey;
+
+ private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
+
+ public DownloadableFile(String path) {
+ super(path);
+ }
+
+ public long getSize() {
+ return super.length();
+ }
+
+ public long getExpectedSize() {
+ if (this.aeskey != null) {
+ if (this.expectedSize == 0) {
+ return 0;
+ } else {
+ return (this.expectedSize / 16 + 1) * 16;
+ }
+ } else {
+ return this.expectedSize;
+ }
+ }
+
+ public void setExpectedSize(long size) {
+ this.expectedSize = size;
+ }
+
+ public String getSha1Sum() {
+ return this.sha1sum;
+ }
+
+ public void setSha1Sum(String sum) {
+ this.sha1sum = sum;
+ }
+
+ public void setKey(byte[] key) {
+ if (key.length == 48) {
+ byte[] secretKey = new byte[32];
+ byte[] iv = new byte[16];
+ System.arraycopy(key, 0, iv, 0, 16);
+ System.arraycopy(key, 16, secretKey, 0, 32);
+ this.aeskey = new SecretKeySpec(secretKey, "AES");
+ this.iv = iv;
+ } else if (key.length >= 32) {
+ byte[] secretKey = new byte[32];
+ System.arraycopy(key, 0, secretKey, 0, 32);
+ this.aeskey = new SecretKeySpec(secretKey, "AES");
+ } else if (key.length >= 16) {
+ byte[] secretKey = new byte[16];
+ System.arraycopy(key, 0, secretKey, 0, 16);
+ this.aeskey = new SecretKeySpec(secretKey, "AES");
+ }
+ }
+
+ public Key getKey() {
+ return this.aeskey;
+ }
+
+ public InputStream createInputStream() {
+ if (this.getKey() == null) {
+ try {
+ return new FileInputStream(this);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ } else {
+ try {
+ IvParameterSpec ips = new IvParameterSpec(iv);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips);
+ Log.d(Config.LOGTAG, "opening encrypted input stream");
+ return new CipherInputStream(new FileInputStream(this), cipher);
+ } catch (NoSuchAlgorithmException e) {
+ Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
+ return null;
+ } catch (NoSuchPaddingException e) {
+ Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
+ return null;
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
+ return null;
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+
+ public OutputStream createOutputStream() {
+ if (this.getKey() == null) {
+ try {
+ return new FileOutputStream(this);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ } else {
+ try {
+ IvParameterSpec ips = new IvParameterSpec(this.iv);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips);
+ Log.d(Config.LOGTAG, "opening encrypted output stream");
+ return new CipherOutputStream(new FileOutputStream(this),
+ cipher);
+ } catch (NoSuchAlgorithmException e) {
+ Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
+ return null;
+ } catch (NoSuchPaddingException e) {
+ Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
+ return null;
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
+ return null;
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java
new file mode 100644
index 00000000..a1872d2f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/ListItem.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.entities;
+
+public interface ListItem extends Comparable<ListItem> {
+ public String getDisplayName();
+
+ public String getJid();
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
new file mode 100644
index 00000000..a390c7ca
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -0,0 +1,478 @@
+package eu.siacs.conversations.entities;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+
+public class Message extends AbstractEntity {
+
+ public static final String TABLENAME = "messages";
+
+ public static final int STATUS_RECEIVED = 0;
+ public static final int STATUS_UNSEND = 1;
+ public static final int STATUS_SEND = 2;
+ public static final int STATUS_SEND_FAILED = 3;
+ public static final int STATUS_SEND_REJECTED = 4;
+ public static final int STATUS_WAITING = 5;
+ public static final int STATUS_OFFERED = 6;
+ public static final int STATUS_SEND_RECEIVED = 7;
+ public static final int STATUS_SEND_DISPLAYED = 8;
+
+ public static final int ENCRYPTION_NONE = 0;
+ public static final int ENCRYPTION_PGP = 1;
+ public static final int ENCRYPTION_OTR = 2;
+ public static final int ENCRYPTION_DECRYPTED = 3;
+ public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
+
+ public static final int TYPE_TEXT = 0;
+ public static final int TYPE_IMAGE = 1;
+ public static final int TYPE_AUDIO = 2;
+ public static final int TYPE_STATUS = 3;
+ public static final int TYPE_PRIVATE = 4;
+
+ public static String CONVERSATION = "conversationUuid";
+ public static String COUNTERPART = "counterpart";
+ public static String TRUE_COUNTERPART = "trueCounterpart";
+ public static String BODY = "body";
+ public static String TIME_SENT = "timeSent";
+ public static String ENCRYPTION = "encryption";
+ public static String STATUS = "status";
+ public static String TYPE = "type";
+ public static String REMOTE_MSG_ID = "remoteMsgId";
+
+ protected String conversationUuid;
+ protected String counterpart;
+ protected String trueCounterpart;
+ protected String body;
+ protected String encryptedBody;
+ protected long timeSent;
+ protected int encryption;
+ protected int status;
+ protected int type;
+ protected boolean read = true;
+ protected String remoteMsgId = null;
+
+ protected Conversation conversation = null;
+ protected Downloadable downloadable = null;
+ public boolean markable = false;
+
+ private Message mNextMessage = null;
+ private Message mPreviousMessage = null;
+
+ private Message() {
+
+ }
+
+ public Message(Conversation conversation, String body, int encryption) {
+ this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
+ conversation.getContactJid(), null, body, System
+ .currentTimeMillis(), encryption,
+ Message.STATUS_UNSEND, TYPE_TEXT, null);
+ this.conversation = conversation;
+ }
+
+ public Message(Conversation conversation, String counterpart, String body,
+ int encryption, int status) {
+ this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
+ counterpart, null, body, System.currentTimeMillis(),
+ encryption, status, TYPE_TEXT, null);
+ this.conversation = conversation;
+ }
+
+ public Message(String uuid, String conversationUUid, String counterpart,
+ String trueCounterpart, String body, long timeSent, int encryption,
+ int status, int type, String remoteMsgId) {
+ this.uuid = uuid;
+ this.conversationUuid = conversationUUid;
+ this.counterpart = counterpart;
+ this.trueCounterpart = trueCounterpart;
+ this.body = body;
+ this.timeSent = timeSent;
+ this.encryption = encryption;
+ this.status = status;
+ this.type = type;
+ this.remoteMsgId = remoteMsgId;
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(CONVERSATION, conversationUuid);
+ values.put(COUNTERPART, counterpart);
+ values.put(TRUE_COUNTERPART, trueCounterpart);
+ values.put(BODY, body);
+ values.put(TIME_SENT, timeSent);
+ values.put(ENCRYPTION, encryption);
+ values.put(STATUS, status);
+ values.put(TYPE, type);
+ values.put(REMOTE_MSG_ID, remoteMsgId);
+ return values;
+ }
+
+ public String getConversationUuid() {
+ return conversationUuid;
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+
+ public String getCounterpart() {
+ return counterpart;
+ }
+
+ public Contact getContact() {
+ if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
+ return this.conversation.getContact();
+ } else {
+ if (this.trueCounterpart == null) {
+ return null;
+ } else {
+ return this.conversation.getAccount().getRoster()
+ .getContactFromRoster(this.trueCounterpart);
+ }
+ }
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public String getReadableBody(Context context) {
+ if (encryption == ENCRYPTION_PGP) {
+ return context.getText(R.string.encrypted_message_received)
+ .toString();
+ } else if (encryption == ENCRYPTION_DECRYPTION_FAILED) {
+ return context.getText(R.string.decryption_failed).toString();
+ } else if (type == TYPE_IMAGE) {
+ return context.getText(R.string.image_file).toString();
+ } else {
+ return body.trim();
+ }
+ }
+
+ public long getTimeSent() {
+ return timeSent;
+ }
+
+ public int getEncryption() {
+ return encryption;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public String getRemoteMsgId() {
+ return this.remoteMsgId;
+ }
+
+ public void setRemoteMsgId(String id) {
+ this.remoteMsgId = id;
+ }
+
+ public static Message fromCursor(Cursor cursor) {
+ return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(CONVERSATION)),
+ cursor.getString(cursor.getColumnIndex(COUNTERPART)),
+ cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)),
+ cursor.getString(cursor.getColumnIndex(BODY)),
+ cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
+ cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(TYPE)),
+ cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)));
+ }
+
+ public void setConversation(Conversation conv) {
+ this.conversation = conv;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public boolean isRead() {
+ return this.read;
+ }
+
+ public void markRead() {
+ this.read = true;
+ }
+
+ public void markUnread() {
+ this.read = false;
+ }
+
+ public void setTime(long time) {
+ this.timeSent = time;
+ }
+
+ public void setEncryption(int encryption) {
+ this.encryption = encryption;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public String getEncryptedBody() {
+ return this.encryptedBody;
+ }
+
+ public void setEncryptedBody(String body) {
+ this.encryptedBody = body;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public void setPresence(String presence) {
+ if (presence == null) {
+ this.counterpart = this.counterpart.split("/", 2)[0];
+ } else {
+ this.counterpart = this.counterpart.split("/", 2)[0] + "/"
+ + presence;
+ }
+ }
+
+ public void setTrueCounterpart(String trueCounterpart) {
+ this.trueCounterpart = trueCounterpart;
+ }
+
+ public String getPresence() {
+ String[] counterparts = this.counterpart.split("/", 2);
+ if (counterparts.length == 2) {
+ return counterparts[1];
+ } else {
+ if (this.counterpart.contains("/")) {
+ return "";
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public void setDownloadable(Downloadable downloadable) {
+ this.downloadable = downloadable;
+ }
+
+ public Downloadable getDownloadable() {
+ return this.downloadable;
+ }
+
+ public static Message createStatusMessage(Conversation conversation) {
+ Message message = new Message();
+ message.setType(Message.TYPE_STATUS);
+ message.setConversation(conversation);
+ return message;
+ }
+
+ public void setCounterpart(String counterpart) {
+ this.counterpart = counterpart;
+ }
+
+ public boolean equals(Message message) {
+ if ((this.remoteMsgId != null) && (this.body != null)
+ && (this.counterpart != null)) {
+ return this.remoteMsgId.equals(message.getRemoteMsgId())
+ && this.body.equals(message.getBody())
+ && this.counterpart.equals(message.getCounterpart());
+ } else {
+ return false;
+ }
+ }
+
+ public Message next() {
+ if (this.mNextMessage == null) {
+ synchronized (this.conversation.messages) {
+ int index = this.conversation.messages.indexOf(this);
+ if (index < 0
+ || index >= this.conversation.getMessages().size() - 1) {
+ this.mNextMessage = null;
+ } else {
+ this.mNextMessage = this.conversation.messages
+ .get(index + 1);
+ }
+ }
+ }
+ return this.mNextMessage;
+ }
+
+ public Message prev() {
+ if (this.mPreviousMessage == null) {
+ synchronized (this.conversation.messages) {
+ int index = this.conversation.messages.indexOf(this);
+ if (index <= 0 || index > this.conversation.messages.size()) {
+ this.mPreviousMessage = null;
+ } else {
+ this.mPreviousMessage = this.conversation.messages
+ .get(index - 1);
+ }
+ }
+ }
+ return this.mPreviousMessage;
+ }
+
+ public boolean mergable(Message message) {
+ if (message == null) {
+ return false;
+ }
+ return (message.getType() == Message.TYPE_TEXT
+ && this.getDownloadable() == null
+ && message.getDownloadable() == null
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && this.getType() == message.getType()
+ && this.getEncryption() == message.getEncryption()
+ && this.getCounterpart().equals(message.getCounterpart())
+ && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && ((this
+ .getStatus() == message.getStatus() || ((this.getStatus() == Message.STATUS_SEND || this
+ .getStatus() == Message.STATUS_SEND_RECEIVED) && (message
+ .getStatus() == Message.STATUS_UNSEND
+ || message.getStatus() == Message.STATUS_SEND || message
+ .getStatus() == Message.STATUS_SEND_DISPLAYED)))));
+ }
+
+ public String getMergedBody() {
+ Message next = this.next();
+ if (this.mergable(next)) {
+ return body.trim() + '\n' + next.getMergedBody();
+ }
+ return body.trim();
+ }
+
+ public int getMergedStatus() {
+ Message next = this.next();
+ if (this.mergable(next)) {
+ return next.getMergedStatus();
+ } else {
+ return getStatus();
+ }
+ }
+
+ public long getMergedTimeSent() {
+ Message next = this.next();
+ if (this.mergable(next)) {
+ return next.getMergedTimeSent();
+ } else {
+ return getTimeSent();
+ }
+ }
+
+ public boolean wasMergedIntoPrevious() {
+ Message prev = this.prev();
+ if (prev == null) {
+ return false;
+ } else {
+ return prev.mergable(this);
+ }
+ }
+
+ public boolean bodyContainsDownloadable() {
+ Contact contact = this.getContact();
+ if (status <= STATUS_RECEIVED
+ && (contact == null || !contact.trusted())) {
+ return false;
+ }
+ try {
+ URL url = new URL(this.getBody());
+ if (!url.getProtocol().equalsIgnoreCase("http")
+ && !url.getProtocol().equalsIgnoreCase("https")) {
+ return false;
+ }
+ if (url.getPath() == null) {
+ return false;
+ }
+ String[] pathParts = url.getPath().split("/");
+ String filename = pathParts[pathParts.length - 1];
+ String[] extensionParts = filename.split("\\.");
+ if (extensionParts.length == 2
+ && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+ extensionParts[extensionParts.length - 1])) {
+ return true;
+ } else if (extensionParts.length == 3
+ && Arrays
+ .asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
+ .contains(extensionParts[extensionParts.length - 1])
+ && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+ extensionParts[extensionParts.length - 2])) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (MalformedURLException e) {
+ return false;
+ }
+ }
+
+ public ImageParams getImageParams() {
+ ImageParams params = new ImageParams();
+ if (this.downloadable != null) {
+ params.size = this.downloadable.getFileSize();
+ }
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split(",");
+ if (parts.length == 1) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ params.origin = parts[0];
+ }
+ } else if (parts.length == 3) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[1]);
+ } catch (NumberFormatException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ params.height = 0;
+ }
+ } else if (parts.length == 4) {
+ params.origin = parts[0];
+ try {
+ params.size = Long.parseLong(parts[1]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[3]);
+ } catch (NumberFormatException e) {
+ params.height = 0;
+ }
+ }
+ return params;
+ }
+
+ public class ImageParams {
+ public long size = 0;
+ public int width = 0;
+ public int height = 0;
+ public String origin;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
new file mode 100644
index 00000000..d7407cd5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -0,0 +1,369 @@
+package eu.siacs.conversations.entities;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import android.annotation.SuppressLint;
+
+@SuppressLint("DefaultLocale")
+public class MucOptions {
+ public static final int ERROR_NO_ERROR = 0;
+ public static final int ERROR_NICK_IN_USE = 1;
+ public static final int ERROR_ROOM_NOT_FOUND = 2;
+ public static final int ERROR_PASSWORD_REQUIRED = 3;
+ public static final int ERROR_BANNED = 4;
+ public static final int ERROR_MEMBERS_ONLY = 5;
+
+ public static final int KICKED_FROM_ROOM = 9;
+
+ public static final String STATUS_CODE_BANNED = "301";
+ public static final String STATUS_CODE_KICKED = "307";
+
+ public interface OnRenameListener {
+ public void onRename(boolean success);
+ }
+
+ public class User {
+ public static final int ROLE_MODERATOR = 3;
+ public static final int ROLE_NONE = 0;
+ public static final int ROLE_PARTICIPANT = 2;
+ public static final int ROLE_VISITOR = 1;
+ public static final int AFFILIATION_ADMIN = 4;
+ public static final int AFFILIATION_OWNER = 3;
+ public static final int AFFILIATION_MEMBER = 2;
+ public static final int AFFILIATION_OUTCAST = 1;
+ public static final int AFFILIATION_NONE = 0;
+
+ private int role;
+ private int affiliation;
+ private String name;
+ private String jid;
+ private long pgpKeyId = 0;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String user) {
+ this.name = user;
+ }
+
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ public String getJid() {
+ return this.jid;
+ }
+
+ public int getRole() {
+ return this.role;
+ }
+
+ public void setRole(String role) {
+ role = role.toLowerCase();
+ if (role.equals("moderator")) {
+ this.role = ROLE_MODERATOR;
+ } else if (role.equals("participant")) {
+ this.role = ROLE_PARTICIPANT;
+ } else if (role.equals("visitor")) {
+ this.role = ROLE_VISITOR;
+ } else {
+ this.role = ROLE_NONE;
+ }
+ }
+
+ public int getAffiliation() {
+ return this.affiliation;
+ }
+
+ public void setAffiliation(String affiliation) {
+ if (affiliation.equalsIgnoreCase("admin")) {
+ this.affiliation = AFFILIATION_ADMIN;
+ } else if (affiliation.equalsIgnoreCase("owner")) {
+ this.affiliation = AFFILIATION_OWNER;
+ } else if (affiliation.equalsIgnoreCase("member")) {
+ this.affiliation = AFFILIATION_MEMBER;
+ } else if (affiliation.equalsIgnoreCase("outcast")) {
+ this.affiliation = AFFILIATION_OUTCAST;
+ } else {
+ this.affiliation = AFFILIATION_NONE;
+ }
+ }
+
+ public void setPgpKeyId(long id) {
+ this.pgpKeyId = id;
+ }
+
+ public long getPgpKeyId() {
+ return this.pgpKeyId;
+ }
+
+ public Contact getContact() {
+ return account.getRoster().getContactFromRoster(getJid());
+ }
+ }
+
+ private Account account;
+ private List<User> users = new CopyOnWriteArrayList<User>();
+ private Conversation conversation;
+ private boolean isOnline = false;
+ private int error = ERROR_ROOM_NOT_FOUND;
+ private OnRenameListener renameListener = null;
+ private boolean aboutToRename = false;
+ private User self = new User();
+ private String subject = null;
+ private String joinnick;
+ private String password = null;
+
+ public MucOptions(Conversation conversation) {
+ this.account = conversation.getAccount();
+ this.conversation = conversation;
+ }
+
+ public void deleteUser(String name) {
+ for (int i = 0; i < users.size(); ++i) {
+ if (users.get(i).getName().equals(name)) {
+ users.remove(i);
+ return;
+ }
+ }
+ }
+
+ public void addUser(User user) {
+ for (int i = 0; i < users.size(); ++i) {
+ if (users.get(i).getName().equals(user.getName())) {
+ users.set(i, user);
+ return;
+ }
+ }
+ users.add(user);
+ }
+
+ public void processPacket(PresencePacket packet, PgpEngine pgp) {
+ String[] fromParts = packet.getFrom().split("/", 2);
+ if (fromParts.length >= 2) {
+ String name = fromParts[1];
+ String type = packet.getAttribute("type");
+ if (type == null) {
+ User user = new User();
+ Element item = packet.findChild("x",
+ "http://jabber.org/protocol/muc#user")
+ .findChild("item");
+ user.setName(name);
+ user.setAffiliation(item.getAttribute("affiliation"));
+ user.setRole(item.getAttribute("role"));
+ user.setJid(item.getAttribute("jid"));
+ user.setName(name);
+ if (name.equals(this.joinnick)) {
+ this.isOnline = true;
+ this.error = ERROR_NO_ERROR;
+ self = user;
+ if (aboutToRename) {
+ if (renameListener != null) {
+ renameListener.onRename(true);
+ }
+ aboutToRename = false;
+ }
+ } else {
+ addUser(user);
+ }
+ if (pgp != null) {
+ Element x = packet.findChild("x", "jabber:x:signed");
+ if (x != null) {
+ Element status = packet.findChild("status");
+ String msg;
+ if (status != null) {
+ msg = status.getContent();
+ } else {
+ msg = "";
+ }
+ user.setPgpKeyId(pgp.fetchKeyId(account, msg,
+ x.getContent()));
+ }
+ }
+ } else if (type.equals("unavailable") && name.equals(this.joinnick)) {
+ Element x = packet.findChild("x",
+ "http://jabber.org/protocol/muc#user");
+ if (x != null) {
+ Element status = x.findChild("status");
+ if (status != null) {
+ String code = status.getAttribute("code");
+ if (STATUS_CODE_KICKED.equals(code)) {
+ this.isOnline = false;
+ this.error = KICKED_FROM_ROOM;
+ } else if (STATUS_CODE_BANNED.equals(code)) {
+ this.isOnline = false;
+ this.error = ERROR_BANNED;
+ }
+ }
+ }
+ } else if (type.equals("unavailable")) {
+ deleteUser(packet.getAttribute("from").split("/", 2)[1]);
+ } else if (type.equals("error")) {
+ Element error = packet.findChild("error");
+ if (error != null && error.hasChild("conflict")) {
+ if (aboutToRename) {
+ if (renameListener != null) {
+ renameListener.onRename(false);
+ }
+ aboutToRename = false;
+ this.setJoinNick(getActualNick());
+ } else {
+ this.error = ERROR_NICK_IN_USE;
+ }
+ } else if (error != null && error.hasChild("not-authorized")) {
+ this.error = ERROR_PASSWORD_REQUIRED;
+ } else if (error != null && error.hasChild("forbidden")) {
+ this.error = ERROR_BANNED;
+ } else if (error != null
+ && error.hasChild("registration-required")) {
+ this.error = ERROR_MEMBERS_ONLY;
+ }
+ }
+ }
+ }
+
+ public List<User> getUsers() {
+ return this.users;
+ }
+
+ public String getProposedNick() {
+ String[] mucParts = conversation.getContactJid().split("/", 2);
+ if (conversation.getBookmark() != null
+ && conversation.getBookmark().getNick() != null) {
+ return conversation.getBookmark().getNick();
+ } else {
+ if (mucParts.length == 2) {
+ return mucParts[1];
+ } else {
+ return account.getUsername();
+ }
+ }
+ }
+
+ public String getActualNick() {
+ if (this.self.getName() != null) {
+ return this.self.getName();
+ } else {
+ return this.getProposedNick();
+ }
+ }
+
+ public void setJoinNick(String nick) {
+ this.joinnick = nick;
+ }
+
+ public boolean online() {
+ return this.isOnline;
+ }
+
+ public int getError() {
+ return this.error;
+ }
+
+ public void setOnRenameListener(OnRenameListener listener) {
+ this.renameListener = listener;
+ }
+
+ public OnRenameListener getOnRenameListener() {
+ return this.renameListener;
+ }
+
+ public void setOffline() {
+ this.users.clear();
+ this.error = 0;
+ this.isOnline = false;
+ }
+
+ public User getSelf() {
+ return self;
+ }
+
+ public void setSubject(String content) {
+ this.subject = content;
+ }
+
+ public String getSubject() {
+ return this.subject;
+ }
+
+ public void flagAboutToRename() {
+ this.aboutToRename = true;
+ }
+
+ public long[] getPgpKeyIds() {
+ List<Long> ids = new ArrayList<Long>();
+ for (User user : getUsers()) {
+ if (user.getPgpKeyId() != 0) {
+ ids.add(user.getPgpKeyId());
+ }
+ }
+ long[] primitivLongArray = new long[ids.size()];
+ for (int i = 0; i < ids.size(); ++i) {
+ primitivLongArray[i] = ids.get(i);
+ }
+ return primitivLongArray;
+ }
+
+ public boolean pgpKeysInUse() {
+ for (User user : getUsers()) {
+ if (user.getPgpKeyId() != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean everybodyHasKeys() {
+ for (User user : getUsers()) {
+ if (user.getPgpKeyId() == 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public String getJoinJid() {
+ return this.conversation.getContactJid().split("/", 2)[0] + "/"
+ + this.joinnick;
+ }
+
+ public String getTrueCounterpart(String counterpart) {
+ for (User user : this.getUsers()) {
+ if (user.getName().equals(counterpart)) {
+ return user.getJid();
+ }
+ }
+ return null;
+ }
+
+ public String getPassword() {
+ this.password = conversation
+ .getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
+ if (this.password == null && conversation.getBookmark() != null
+ && conversation.getBookmark().getPassword() != null) {
+ return conversation.getBookmark().getPassword();
+ } else {
+ return this.password;
+ }
+ }
+
+ public void setPassword(String password) {
+ if (conversation.getBookmark() != null) {
+ conversation.getBookmark().setPassword(password);
+ } else {
+ this.password = password;
+ }
+ conversation
+ .setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java
new file mode 100644
index 00000000..b5899847
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Presences.java
@@ -0,0 +1,76 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Presences {
+
+ public static final int CHAT = -1;
+ public static final int ONLINE = 0;
+ public static final int AWAY = 1;
+ public static final int XA = 2;
+ public static final int DND = 3;
+ public static final int OFFLINE = 4;
+
+ private Hashtable<String, Integer> presences = new Hashtable<String, Integer>();
+
+ public Hashtable<String, Integer> getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.put(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.remove(resource);
+ }
+
+ public void clearPresences() {
+ this.presences.clear();
+ }
+
+ public int getMostAvailableStatus() {
+ int status = OFFLINE;
+ Iterator<Entry<String, Integer>> it = presences.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, Integer> entry = it.next();
+ if (entry.getValue() < status)
+ status = entry.getValue();
+ }
+ return status;
+ }
+
+ public static int parseShow(Element show) {
+ if ((show == null) || (show.getContent() == null)) {
+ return Presences.ONLINE;
+ } else if (show.getContent().equals("away")) {
+ return Presences.AWAY;
+ } else if (show.getContent().equals("xa")) {
+ return Presences.XA;
+ } else if (show.getContent().equals("chat")) {
+ return Presences.CHAT;
+ } else if (show.getContent().equals("dnd")) {
+ return Presences.DND;
+ } else {
+ return Presences.OFFLINE;
+ }
+ }
+
+ public int size() {
+ return presences.size();
+ }
+
+ public String[] asStringArray() {
+ final String[] presencesArray = new String[presences.size()];
+ presences.keySet().toArray(presencesArray);
+ return presencesArray;
+ }
+
+ public boolean has(String presence) {
+ return presences.containsKey(presence);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java
new file mode 100644
index 00000000..3267b15a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Roster.java
@@ -0,0 +1,83 @@
+package eu.siacs.conversations.entities;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class Roster {
+ Account account;
+ ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<String, Contact>();
+ private String version = null;
+
+ public Roster(Account account) {
+ this.account = account;
+ }
+
+ public Contact getContactFromRoster(String jid) {
+ if (jid == null) {
+ return null;
+ }
+ String cleanJid = jid.split("/", 2)[0];
+ Contact contact = contacts.get(cleanJid);
+ if (contact != null && contact.showInRoster()) {
+ return contact;
+ } else {
+ return null;
+ }
+ }
+
+ public Contact getContact(String jid) {
+ String cleanJid = jid.split("/", 2)[0].toLowerCase(Locale.getDefault());
+ if (contacts.containsKey(cleanJid)) {
+ return contacts.get(cleanJid);
+ } else {
+ Contact contact = new Contact(cleanJid);
+ contact.setAccount(account);
+ contacts.put(cleanJid, contact);
+ return contact;
+ }
+ }
+
+ public void clearPresences() {
+ for (Contact contact : getContacts()) {
+ contact.clearPresences();
+ }
+ }
+
+ public void markAllAsNotInRoster() {
+ for (Contact contact : getContacts()) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ }
+ }
+
+ public void clearSystemAccounts() {
+ for (Contact contact : getContacts()) {
+ contact.setPhotoUri(null);
+ contact.setSystemName(null);
+ contact.setSystemAccount(null);
+ }
+ }
+
+ public List<Contact> getContacts() {
+ return new ArrayList<Contact>(this.contacts.values());
+ }
+
+ public void initContact(Contact contact) {
+ contact.setAccount(account);
+ contact.setOption(Contact.Options.IN_ROSTER);
+ contacts.put(contact.getJid(), contact);
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
new file mode 100644
index 00000000..c96d116d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -0,0 +1,48 @@
+package eu.siacs.conversations.generator;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+
+import android.util.Base64;
+
+public abstract class AbstractGenerator {
+ public final String[] FEATURES = { "urn:xmpp:jingle:1",
+ "urn:xmpp:jingle:apps:file-transfer:3",
+ "urn:xmpp:jingle:transports:s5b:1",
+ "urn:xmpp:jingle:transports:ibb:1", "urn:xmpp:receipts",
+ "urn:xmpp:chat-markers:0", "http://jabber.org/protocol/muc",
+ "jabber:x:conference", "http://jabber.org/protocol/caps",
+ "http://jabber.org/protocol/disco#info",
+ "urn:xmpp:avatar:metadata+notify" };
+ public final String IDENTITY_NAME = "Conversations 0.7";
+ public final String IDENTITY_TYPE = "phone";
+
+ protected XmppConnectionService mXmppConnectionService;
+
+ protected AbstractGenerator(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public String getCapHash() {
+ StringBuilder s = new StringBuilder();
+ s.append("client/" + IDENTITY_TYPE + "//" + IDENTITY_NAME + "<");
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ List<String> features = Arrays.asList(FEATURES);
+ Collections.sort(features);
+ for (String feature : features) {
+ s.append(feature + "<");
+ }
+ byte[] sha1 = md.digest(s.toString().getBytes());
+ return new String(Base64.encode(sha1, Base64.DEFAULT)).trim();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
new file mode 100644
index 00000000..d44bf0ca
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -0,0 +1,96 @@
+package eu.siacs.conversations.generator;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqGenerator extends AbstractGenerator {
+
+ public IqGenerator(XmppConnectionService service) {
+ super(service);
+ }
+
+ public IqPacket discoResponse(IqPacket request) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT);
+ packet.setId(request.getId());
+ packet.setTo(request.getFrom());
+ Element query = packet.addChild("query",
+ "http://jabber.org/protocol/disco#info");
+ query.setAttribute("node", request.query().getAttribute("node"));
+ Element identity = query.addChild("identity");
+ identity.setAttribute("category", "client");
+ identity.setAttribute("type", this.IDENTITY_TYPE);
+ identity.setAttribute("name", IDENTITY_NAME);
+ List<String> features = Arrays.asList(FEATURES);
+ Collections.sort(features);
+ for (String feature : features) {
+ query.addChild("feature").setAttribute("var", feature);
+ }
+ return packet;
+ }
+
+ protected IqPacket publish(String node, Element item) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE_SET);
+ Element pubsub = packet.addChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ Element publish = pubsub.addChild("publish");
+ publish.setAttribute("node", node);
+ publish.addChild(item);
+ return packet;
+ }
+
+ protected IqPacket retrieve(String node, Element item) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE_GET);
+ Element pubsub = packet.addChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ Element items = pubsub.addChild("items");
+ items.setAttribute("node", node);
+ if (item != null) {
+ items.addChild(item);
+ }
+ return packet;
+ }
+
+ public IqPacket publishAvatar(Avatar avatar) {
+ Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ Element data = item.addChild("data", "urn:xmpp:avatar:data");
+ data.setContent(avatar.image);
+ return publish("urn:xmpp:avatar:data", item);
+ }
+
+ public IqPacket publishAvatarMetadata(Avatar avatar) {
+ Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ Element metadata = item
+ .addChild("metadata", "urn:xmpp:avatar:metadata");
+ Element info = metadata.addChild("info");
+ info.setAttribute("bytes", avatar.size);
+ info.setAttribute("id", avatar.sha1sum);
+ info.setAttribute("height", avatar.height);
+ info.setAttribute("width", avatar.height);
+ info.setAttribute("type", avatar.type);
+ return publish("urn:xmpp:avatar:metadata", item);
+ }
+
+ public IqPacket retrieveAvatar(Avatar avatar) {
+ Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ IqPacket packet = retrieve("urn:xmpp:avatar:data", item);
+ packet.setTo(avatar.owner);
+ return packet;
+ }
+
+ public IqPacket retrieveAvatarMetaData(String to) {
+ IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null);
+ if (to != null) {
+ packet.setTo(to);
+ }
+ return packet;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
new file mode 100644
index 00000000..dd833e56
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -0,0 +1,178 @@
+package eu.siacs.conversations.generator;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageGenerator extends AbstractGenerator {
+ public MessageGenerator(XmppConnectionService service) {
+ super(service);
+ }
+
+ private MessagePacket preparePacket(Message message, boolean addDelay) {
+ Conversation conversation = message.getConversation();
+ Account account = conversation.getAccount();
+ MessagePacket packet = new MessagePacket();
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ packet.setTo(message.getCounterpart());
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.addChild("markable", "urn:xmpp:chat-markers:0");
+ if (this.mXmppConnectionService.indicateReceived()) {
+ packet.addChild("request", "urn:xmpp:receipts");
+ }
+ } else if (message.getType() == Message.TYPE_PRIVATE) {
+ packet.setTo(message.getCounterpart());
+ packet.setType(MessagePacket.TYPE_CHAT);
+ } else {
+ packet.setTo(message.getCounterpart().split("/", 2)[0]);
+ packet.setType(MessagePacket.TYPE_GROUPCHAT);
+ }
+ packet.setFrom(account.getFullJid());
+ packet.setId(message.getUuid());
+ if (addDelay) {
+ addDelay(packet, message.getTimeSent());
+ }
+ return packet;
+ }
+
+ private void addDelay(MessagePacket packet, long timestamp) {
+ final SimpleDateFormat mDateFormat = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ Element delay = packet.addChild("delay", "urn:xmpp:delay");
+ Date date = new Date(timestamp);
+ delay.setAttribute("stamp", mDateFormat.format(date));
+ }
+
+ public MessagePacket generateOtrChat(Message message) {
+ return generateOtrChat(message, false);
+ }
+
+ public MessagePacket generateOtrChat(Message message, boolean addDelay) {
+ Session otrSession = message.getConversation().getOtrSession();
+ if (otrSession == null) {
+ return null;
+ }
+ MessagePacket packet = preparePacket(message, addDelay);
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ try {
+ packet.setBody(otrSession.transformSending(message.getBody()));
+ return packet;
+ } catch (OtrException e) {
+ return null;
+ }
+ }
+
+ public MessagePacket generateChat(Message message) {
+ return generateChat(message, false);
+ }
+
+ public MessagePacket generateChat(Message message, boolean addDelay) {
+ MessagePacket packet = preparePacket(message, addDelay);
+ packet.setBody(message.getBody());
+ return packet;
+ }
+
+ public MessagePacket generatePgpChat(Message message) {
+ return generatePgpChat(message, false);
+ }
+
+ public MessagePacket generatePgpChat(Message message, boolean addDelay) {
+ MessagePacket packet = preparePacket(message, addDelay);
+ packet.setBody("This is an XEP-0027 encryted message");
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ packet.addChild("x", "jabber:x:encrypted").setContent(
+ message.getEncryptedBody());
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ packet.addChild("x", "jabber:x:encrypted").setContent(
+ message.getBody());
+ }
+ return packet;
+ }
+
+ public MessagePacket generateNotAcceptable(MessagePacket origin) {
+ MessagePacket packet = generateError(origin);
+ Element error = packet.addChild("error");
+ error.setAttribute("type", "modify");
+ error.setAttribute("code", "406");
+ error.addChild("not-acceptable");
+ return packet;
+ }
+
+ private MessagePacket generateError(MessagePacket origin) {
+ MessagePacket packet = new MessagePacket();
+ packet.setId(origin.getId());
+ packet.setTo(origin.getFrom());
+ packet.setBody(origin.getBody());
+ packet.setType(MessagePacket.TYPE_ERROR);
+ return packet;
+ }
+
+ public MessagePacket confirm(Account account, String to, String id) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_NORMAL);
+ packet.setTo(to);
+ packet.setFrom(account.getFullJid());
+ Element received = packet.addChild("displayed",
+ "urn:xmpp:chat-markers:0");
+ received.setAttribute("id", id);
+ return packet;
+ }
+
+ public MessagePacket conferenceSubject(Conversation conversation,
+ String subject) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_GROUPCHAT);
+ packet.setTo(conversation.getContactJid().split("/", 2)[0]);
+ Element subjectChild = new Element("subject");
+ subjectChild.setContent(subject);
+ packet.addChild(subjectChild);
+ packet.setFrom(conversation.getAccount().getJid());
+ return packet;
+ }
+
+ public MessagePacket directInvite(Conversation conversation, String contact) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_NORMAL);
+ packet.setTo(contact);
+ packet.setFrom(conversation.getAccount().getFullJid());
+ Element x = packet.addChild("x", "jabber:x:conference");
+ x.setAttribute("jid", conversation.getContactJid().split("/", 2)[0]);
+ return packet;
+ }
+
+ public MessagePacket invite(Conversation conversation, String contact) {
+ MessagePacket packet = new MessagePacket();
+ packet.setTo(conversation.getContactJid().split("/", 2)[0]);
+ packet.setFrom(conversation.getAccount().getFullJid());
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
+ Element invite = new Element("invite");
+ invite.setAttribute("to", contact);
+ x.addChild(invite);
+ packet.addChild(x);
+ return packet;
+ }
+
+ public MessagePacket received(Account account,
+ MessagePacket originalMessage, String namespace) {
+ MessagePacket receivedPacket = new MessagePacket();
+ receivedPacket.setType(MessagePacket.TYPE_NORMAL);
+ receivedPacket.setTo(originalMessage.getFrom());
+ receivedPacket.setFrom(account.getFullJid());
+ Element received = receivedPacket.addChild("received", namespace);
+ received.setAttribute("id", originalMessage.getId());
+ return receivedPacket;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
new file mode 100644
index 00000000..d896dd00
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
@@ -0,0 +1,57 @@
+package eu.siacs.conversations.generator;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public class PresenceGenerator extends AbstractGenerator {
+
+ public PresenceGenerator(XmppConnectionService service) {
+ super(service);
+ }
+
+ private PresencePacket subscription(String type, Contact contact) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("type", type);
+ packet.setAttribute("to", contact.getJid());
+ packet.setAttribute("from", contact.getAccount().getJid());
+ return packet;
+ }
+
+ public PresencePacket requestPresenceUpdatesFrom(Contact contact) {
+ return subscription("subscribe", contact);
+ }
+
+ public PresencePacket stopPresenceUpdatesFrom(Contact contact) {
+ return subscription("unsubscribe", contact);
+ }
+
+ public PresencePacket stopPresenceUpdatesTo(Contact contact) {
+ return subscription("unsubscribed", contact);
+ }
+
+ public PresencePacket sendPresenceUpdatesTo(Contact contact) {
+ return subscription("subscribed", contact);
+ }
+
+ public PresencePacket sendPresence(Account account) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("from", account.getFullJid());
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ String capHash = getCapHash();
+ if (capHash != null) {
+ Element cap = packet.addChild("c",
+ "http://jabber.org/protocol/caps");
+ cap.setAttribute("hash", "sha-1");
+ cap.setAttribute("node", "http://conversions.siacs.eu");
+ cap.setAttribute("ver", capHash);
+ }
+ return packet;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/src/main/java/eu/siacs/conversations/http/HttpConnection.java
new file mode 100644
index 00000000..407a13d9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnection.java
@@ -0,0 +1,255 @@
+package eu.siacs.conversations.http;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class HttpConnection implements Downloadable {
+
+ private HttpConnectionManager mHttpConnectionManager;
+ private XmppConnectionService mXmppConnectionService;
+
+ private URL mUrl;
+ private Message message;
+ private DownloadableFile file;
+ private int mStatus = Downloadable.STATUS_UNKNOWN;
+
+ public HttpConnection(HttpConnectionManager manager) {
+ this.mHttpConnectionManager = manager;
+ this.mXmppConnectionService = manager.getXmppConnectionService();
+ }
+
+ @Override
+ public boolean start() {
+ if (mXmppConnectionService.hasInternetConnection()) {
+ if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
+ checkFileSize(true);
+ } else {
+ new Thread(new FileDownloader(true)).start();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void init(Message message) {
+ this.message = message;
+ this.message.setDownloadable(this);
+ try {
+ mUrl = new URL(message.getBody());
+ this.file = mXmppConnectionService.getFileBackend().getFile(
+ message, false);
+ String reference = mUrl.getRef();
+ if (reference != null && reference.length() == 96) {
+ this.file.setKey(CryptoHelper.hexToBytes(reference));
+ }
+ if (this.message.getEncryption() == Message.ENCRYPTION_OTR
+ && this.file.getKey() == null) {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
+ }
+ checkFileSize(false);
+ } catch (MalformedURLException e) {
+ this.cancel();
+ }
+ }
+
+ private void checkFileSize(boolean interactive) {
+ new Thread(new FileSizeChecker(interactive)).start();
+ }
+
+ public void cancel() {
+ mHttpConnectionManager.finishConnection(this);
+ message.setDownloadable(null);
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void finish() {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ mXmppConnectionService.sendBroadcast(intent);
+ message.setDownloadable(null);
+ mHttpConnectionManager.finishConnection(this);
+ }
+
+ private void changeStatus(int status) {
+ this.mStatus = status;
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void setupTrustManager(HttpsURLConnection connection,
+ boolean interactive) {
+ X509TrustManager trustManager;
+ HostnameVerifier hostnameVerifier;
+ if (interactive) {
+ trustManager = mXmppConnectionService.getMemorizingTrustManager();
+ hostnameVerifier = mXmppConnectionService
+ .getMemorizingTrustManager().wrapHostnameVerifier(
+ new StrictHostnameVerifier());
+ } else {
+ trustManager = mXmppConnectionService.getMemorizingTrustManager()
+ .getNonInteractive();
+ hostnameVerifier = mXmppConnectionService
+ .getMemorizingTrustManager()
+ .wrapHostnameVerifierNonInteractive(
+ new StrictHostnameVerifier());
+ }
+ try {
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null, new X509TrustManager[] { trustManager },
+ mXmppConnectionService.getRNG());
+ connection.setSSLSocketFactory(sc.getSocketFactory());
+ connection.setHostnameVerifier(hostnameVerifier);
+ } catch (KeyManagementException e) {
+ return;
+ } catch (NoSuchAlgorithmException e) {
+ return;
+ }
+ }
+
+ private class FileSizeChecker implements Runnable {
+
+ private boolean interactive = false;
+
+ public FileSizeChecker(boolean interactive) {
+ this.interactive = interactive;
+ }
+
+ @Override
+ public void run() {
+ long size;
+ try {
+ size = retrieveFileSize();
+ } catch (SSLHandshakeException e) {
+ changeStatus(STATUS_OFFER_CHECK_FILESIZE);
+ return;
+ } catch (IOException e) {
+ cancel();
+ return;
+ }
+ file.setExpectedSize(size);
+ if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
+ new Thread(new FileDownloader(interactive)).start();
+ } else {
+ changeStatus(STATUS_OFFER);
+ }
+ }
+
+ private long retrieveFileSize() throws IOException,
+ SSLHandshakeException {
+ changeStatus(STATUS_CHECKING);
+ HttpURLConnection connection = (HttpURLConnection) mUrl
+ .openConnection();
+ connection.setRequestMethod("HEAD");
+ if (connection instanceof HttpsURLConnection) {
+ setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ String contentLength = connection.getHeaderField("Content-Length");
+ if (contentLength == null) {
+ throw new IOException();
+ }
+ try {
+ return Long.parseLong(contentLength, 10);
+ } catch (NumberFormatException e) {
+ throw new IOException();
+ }
+ }
+
+ }
+
+ private class FileDownloader implements Runnable {
+
+ private boolean interactive = false;
+
+ public FileDownloader(boolean interactive) {
+ this.interactive = interactive;
+ }
+
+ @Override
+ public void run() {
+ try {
+ changeStatus(STATUS_DOWNLOADING);
+ download();
+ updateImageBounds();
+ finish();
+ } catch (SSLHandshakeException e) {
+ changeStatus(STATUS_OFFER);
+ } catch (IOException e) {
+ cancel();
+ }
+ }
+
+ private void download() throws SSLHandshakeException, IOException {
+ HttpURLConnection connection = (HttpURLConnection) mUrl
+ .openConnection();
+ if (connection instanceof HttpsURLConnection) {
+ setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ BufferedInputStream is = new BufferedInputStream(
+ connection.getInputStream());
+ OutputStream os = file.createOutputStream();
+ int count = -1;
+ byte[] buffer = new byte[1024];
+ while ((count = is.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ os.flush();
+ os.close();
+ is.close();
+ }
+
+ private void updateImageBounds() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ message.setBody(mUrl.toString() + "," + file.getSize() + ','
+ + imageWidth + ',' + imageHeight);
+ message.setType(Message.TYPE_IMAGE);
+ mXmppConnectionService.updateMessage(message);
+ }
+
+ }
+
+ @Override
+ public int getStatus() {
+ return this.mStatus;
+ }
+
+ @Override
+ public long getFileSize() {
+ if (this.file != null) {
+ return this.file.getExpectedSize();
+ } else {
+ return 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
new file mode 100644
index 00000000..9a2a2405
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
@@ -0,0 +1,28 @@
+package eu.siacs.conversations.http;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public class HttpConnectionManager extends AbstractConnectionManager {
+
+ public HttpConnectionManager(XmppConnectionService service) {
+ super(service);
+ }
+
+ private List<HttpConnection> connections = new CopyOnWriteArrayList<HttpConnection>();
+
+ 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
new file mode 100644
index 00000000..5541c1c6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
@@ -0,0 +1,92 @@
+package eu.siacs.conversations.parser;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+
+public abstract class AbstractParser {
+
+ protected XmppConnectionService mXmppConnectionService;
+
+ protected AbstractParser(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ protected long getTimestamp(Element packet) {
+ long now = System.currentTimeMillis();
+ ArrayList<String> stamps = new ArrayList<String>();
+ for (Element child : packet.getChildren()) {
+ if (child.getName().equals("delay")) {
+ stamps.add(child.getAttribute("stamp").replace("Z", "+0000"));
+ }
+ }
+ Collections.sort(stamps);
+ if (stamps.size() >= 1) {
+ try {
+ String stamp = stamps.get(stamps.size() - 1);
+ if (stamp.contains(".")) {
+ Date date = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
+ .parse(stamp);
+ if (now < date.getTime()) {
+ return now;
+ } else {
+ return date.getTime();
+ }
+ } else {
+ Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",
+ Locale.US).parse(stamp);
+ if (now < date.getTime()) {
+ return now;
+ } else {
+ return date.getTime();
+ }
+ }
+ } catch (ParseException e) {
+ return now;
+ }
+ } else {
+ return now;
+ }
+ }
+
+ protected void updateLastseen(Element packet, Account account,
+ boolean presenceOverwrite) {
+ String[] fromParts = packet.getAttribute("from").split("/", 2);
+ String from = fromParts[0];
+ String presence = null;
+ if (fromParts.length >= 2) {
+ presence = fromParts[1];
+ } else {
+ presence = "";
+ }
+ Contact contact = account.getRoster().getContact(from);
+ long timestamp = getTimestamp(packet);
+ if (timestamp >= contact.lastseen.time) {
+ contact.lastseen.time = timestamp;
+ if ((presence != null) && (presenceOverwrite)) {
+ contact.lastseen.presence = presence;
+ }
+ }
+ }
+
+ protected String avatarData(Element items) {
+ Element item = items.findChild("item");
+ if (item == null) {
+ return null;
+ }
+ Element data = item.findChild("data", "urn:xmpp:avatar:data");
+ if (data == null) {
+ return null;
+ }
+ return data.getContent();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
new file mode 100644
index 00000000..df6754f2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -0,0 +1,92 @@
+package eu.siacs.conversations.parser;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqParser extends AbstractParser implements OnIqPacketReceived {
+
+ public IqParser(XmppConnectionService service) {
+ super(service);
+ }
+
+ public void rosterItems(Account account, Element query) {
+ String version = query.getAttribute("ver");
+ if (version != null) {
+ account.getRoster().setVersion(version);
+ }
+ for (Element item : query.getChildren()) {
+ if (item.getName().equals("item")) {
+ String jid = item.getAttribute("jid");
+ String name = item.getAttribute("name");
+ String subscription = item.getAttribute("subscription");
+ Contact contact = account.getRoster().getContact(jid);
+ if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ contact.setServerName(name);
+ }
+ if (subscription != null) {
+ if (subscription.equals("remove")) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else {
+ contact.setOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ contact.parseSubscriptionFromElement(item);
+ }
+ }
+ }
+ }
+ mXmppConnectionService.updateRosterUi();
+ }
+
+ public String avatarData(IqPacket packet) {
+ Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub == null) {
+ return null;
+ }
+ Element items = pubsub.findChild("items");
+ if (items == null) {
+ return null;
+ }
+ return super.avatarData(items);
+ }
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.hasChild("query", "jabber:iq:roster")) {
+ String from = packet.getFrom();
+ if ((from == null) || (from.equals(account.getJid()))) {
+ Element query = packet.findChild("query");
+ this.rosterItems(account, query);
+ }
+ } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
+ || packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
+ mXmppConnectionService.getJingleConnectionManager()
+ .deliverIbbPacket(account, packet);
+ } else if (packet.hasChild("query",
+ "http://jabber.org/protocol/disco#info")) {
+ IqPacket response = mXmppConnectionService.getIqGenerator()
+ .discoResponse(packet);
+ account.getXmppConnection().sendIqPacket(response, null);
+ } else if (packet.hasChild("ping", "urn:xmpp:ping")) {
+ IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
+ mXmppConnectionService.sendIqPacket(account, response, null);
+ } else {
+ if ((packet.getType() == IqPacket.TYPE_GET)
+ || (packet.getType() == IqPacket.TYPE_SET)) {
+ IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR);
+ Element error = response.addChild("error");
+ error.setAttribute("type", "cancel");
+ error.addChild("feature-not-implemented",
+ "urn:ietf:params:xml:ns:xmpp-stanzas");
+ account.getXmppConnection().sendIqPacket(response, null);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
new file mode 100644
index 00000000..b5e14305
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -0,0 +1,517 @@
+package eu.siacs.conversations.parser;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.NotificationService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageParser extends AbstractParser implements
+ OnMessagePacketReceived {
+ public MessageParser(XmppConnectionService service) {
+ super(service);
+ }
+
+ private Message parseChat(MessagePacket packet, Account account) {
+ String[] fromParts = packet.getFrom().split("/", 2);
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, fromParts[0], false);
+ updateLastseen(packet, account, true);
+ String pgpBody = getPgpBody(packet);
+ Message finishedMessage;
+ if (pgpBody != null) {
+ finishedMessage = new Message(conversation, packet.getFrom(),
+ pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED);
+ } else {
+ finishedMessage = new Message(conversation, packet.getFrom(),
+ packet.getBody(), Message.ENCRYPTION_NONE,
+ Message.STATUS_RECEIVED);
+ }
+ finishedMessage.setRemoteMsgId(packet.getId());
+ finishedMessage.markable = isMarkable(packet);
+ if (conversation.getMode() == Conversation.MODE_MULTI
+ && fromParts.length >= 2) {
+ finishedMessage.setType(Message.TYPE_PRIVATE);
+ finishedMessage.setPresence(fromParts[1]);
+ finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+ .getTrueCounterpart(fromParts[1]));
+ if (conversation.hasDuplicateMessage(finishedMessage)) {
+ return null;
+ }
+
+ }
+ finishedMessage.setTime(getTimestamp(packet));
+ return finishedMessage;
+ }
+
+ private Message parseOtrChat(MessagePacket packet, Account account) {
+ boolean properlyAddressed = (packet.getTo().split("/", 2).length == 2)
+ || (account.countPresences() == 1);
+ String[] fromParts = packet.getFrom().split("/", 2);
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, fromParts[0], false);
+ String presence;
+ if (fromParts.length >= 2) {
+ presence = fromParts[1];
+ } else {
+ presence = "";
+ }
+ updateLastseen(packet, account, true);
+ String body = packet.getBody();
+ if (body.matches("^\\?OTRv\\d*\\?")) {
+ conversation.endOtrIfNeeded();
+ }
+ if (!conversation.hasValidOtrSession()) {
+ if (properlyAddressed) {
+ conversation.startOtrSession(mXmppConnectionService, presence,
+ false);
+ } else {
+ return null;
+ }
+ } else {
+ String foreignPresence = conversation.getOtrSession()
+ .getSessionID().getUserID();
+ if (!foreignPresence.equals(presence)) {
+ conversation.endOtrIfNeeded();
+ if (properlyAddressed) {
+ conversation.startOtrSession(mXmppConnectionService,
+ presence, false);
+ } else {
+ return null;
+ }
+ }
+ }
+ try {
+ Session otrSession = conversation.getOtrSession();
+ SessionStatus before = otrSession.getSessionStatus();
+ body = otrSession.transformReceiving(body);
+ SessionStatus after = otrSession.getSessionStatus();
+ if ((before != after) && (after == SessionStatus.ENCRYPTED)) {
+ mXmppConnectionService.onOtrSessionEstablished(conversation);
+ } else if ((before != after) && (after == SessionStatus.FINISHED)) {
+ conversation.resetOtrSession();
+ mXmppConnectionService.updateConversationUi();
+ }
+ if ((body == null) || (body.isEmpty())) {
+ return null;
+ }
+ if (body.startsWith(CryptoHelper.FILETRANSFER)) {
+ String key = body.substring(CryptoHelper.FILETRANSFER.length());
+ conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
+ return null;
+ }
+ Message finishedMessage = new Message(conversation,
+ packet.getFrom(), body, Message.ENCRYPTION_OTR,
+ Message.STATUS_RECEIVED);
+ finishedMessage.setTime(getTimestamp(packet));
+ finishedMessage.setRemoteMsgId(packet.getId());
+ finishedMessage.markable = isMarkable(packet);
+ return finishedMessage;
+ } catch (Exception e) {
+ String receivedId = packet.getId();
+ if (receivedId != null) {
+ mXmppConnectionService.replyWithNotAcceptable(account, packet);
+ }
+ conversation.resetOtrSession();
+ return null;
+ }
+ }
+
+ private Message parseGroupchat(MessagePacket packet, Account account) {
+ int status;
+ String[] fromParts = packet.getFrom().split("/", 2);
+ if (mXmppConnectionService.find(account.pendingConferenceLeaves,
+ account, fromParts[0]) != null) {
+ return null;
+ }
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, fromParts[0], true);
+ if (packet.hasChild("subject")) {
+ conversation.getMucOptions().setSubject(
+ packet.findChild("subject").getContent());
+ mXmppConnectionService.updateConversationUi();
+ return null;
+ }
+ if ((fromParts.length == 1)) {
+ return null;
+ }
+ String counterPart = fromParts[1];
+ if (counterPart.equals(conversation.getMucOptions().getActualNick())) {
+ if (mXmppConnectionService.markMessage(conversation,
+ packet.getId(), Message.STATUS_SEND)) {
+ return null;
+ } else {
+ status = Message.STATUS_SEND;
+ }
+ } else {
+ status = Message.STATUS_RECEIVED;
+ }
+ String pgpBody = getPgpBody(packet);
+ Message finishedMessage;
+ if (pgpBody == null) {
+ finishedMessage = new Message(conversation, counterPart,
+ packet.getBody(), Message.ENCRYPTION_NONE, status);
+ } else {
+ finishedMessage = new Message(conversation, counterPart, pgpBody,
+ Message.ENCRYPTION_PGP, status);
+ }
+ finishedMessage.setRemoteMsgId(packet.getId());
+ finishedMessage.markable = isMarkable(packet);
+ if (status == Message.STATUS_RECEIVED) {
+ finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+ .getTrueCounterpart(counterPart));
+ }
+ if (packet.hasChild("delay")
+ && conversation.hasDuplicateMessage(finishedMessage)) {
+ return null;
+ }
+ finishedMessage.setTime(getTimestamp(packet));
+ return finishedMessage;
+ }
+
+ private Message parseCarbonMessage(MessagePacket packet, Account account) {
+ int status;
+ String fullJid;
+ Element forwarded;
+ if (packet.hasChild("received", "urn:xmpp:carbons:2")) {
+ forwarded = packet.findChild("received", "urn:xmpp:carbons:2")
+ .findChild("forwarded", "urn:xmpp:forward:0");
+ status = Message.STATUS_RECEIVED;
+ } else if (packet.hasChild("sent", "urn:xmpp:carbons:2")) {
+ forwarded = packet.findChild("sent", "urn:xmpp:carbons:2")
+ .findChild("forwarded", "urn:xmpp:forward:0");
+ status = Message.STATUS_SEND;
+ } else {
+ return null;
+ }
+ if (forwarded == null) {
+ return null;
+ }
+ Element message = forwarded.findChild("message");
+ if (message == null) {
+ return null;
+ }
+ if (!message.hasChild("body")) {
+ if (status == Message.STATUS_RECEIVED
+ && message.getAttribute("from") != null) {
+ parseNonMessage(message, account);
+ } else if (status == Message.STATUS_SEND
+ && message.hasChild("displayed", "urn:xmpp:chat-markers:0")) {
+ String to = message.getAttribute("to");
+ if (to != null) {
+ Conversation conversation = mXmppConnectionService.find(
+ mXmppConnectionService.getConversations(), account,
+ to.split("/")[0]);
+ if (conversation != null) {
+ mXmppConnectionService.markRead(conversation, false);
+ }
+ }
+ }
+ return null;
+ }
+ if (status == Message.STATUS_RECEIVED) {
+ fullJid = message.getAttribute("from");
+ if (fullJid == null) {
+ return null;
+ } else {
+ updateLastseen(message, account, true);
+ }
+ } else {
+ fullJid = message.getAttribute("to");
+ if (fullJid == null) {
+ return null;
+ }
+ }
+ String[] parts = fullJid.split("/", 2);
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, parts[0], false);
+ String pgpBody = getPgpBody(message);
+ Message finishedMessage;
+ if (pgpBody != null) {
+ finishedMessage = new Message(conversation, fullJid, pgpBody,
+ Message.ENCRYPTION_PGP, status);
+ } else {
+ String body = message.findChild("body").getContent();
+ finishedMessage = new Message(conversation, fullJid, body,
+ Message.ENCRYPTION_NONE, status);
+ }
+ finishedMessage.setTime(getTimestamp(message));
+ finishedMessage.setRemoteMsgId(message.getAttribute("id"));
+ finishedMessage.markable = isMarkable(message);
+ if (conversation.getMode() == Conversation.MODE_MULTI
+ && parts.length >= 2) {
+ finishedMessage.setType(Message.TYPE_PRIVATE);
+ finishedMessage.setPresence(parts[1]);
+ finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+ .getTrueCounterpart(parts[1]));
+ if (conversation.hasDuplicateMessage(finishedMessage)) {
+ return null;
+ }
+ }
+
+ return finishedMessage;
+ }
+
+ private void parseError(MessagePacket packet, Account account) {
+ String[] fromParts = packet.getFrom().split("/", 2);
+ mXmppConnectionService.markMessage(account, fromParts[0],
+ packet.getId(), Message.STATUS_SEND_FAILED);
+ }
+
+ private void parseNonMessage(Element packet, Account account) {
+ String from = packet.getAttribute("from");
+ if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) {
+ Element event = packet.findChild("event",
+ "http://jabber.org/protocol/pubsub#event");
+ parseEvent(event, packet.getAttribute("from"), account);
+ } else if (from != null
+ && packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) {
+ String id = packet
+ .findChild("displayed", "urn:xmpp:chat-markers:0")
+ .getAttribute("id");
+ updateLastseen(packet, account, true);
+ mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+ id, Message.STATUS_SEND_DISPLAYED);
+ } else if (from != null
+ && packet.hasChild("received", "urn:xmpp:chat-markers:0")) {
+ String id = packet.findChild("received", "urn:xmpp:chat-markers:0")
+ .getAttribute("id");
+ updateLastseen(packet, account, false);
+ mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+ id, Message.STATUS_SEND_RECEIVED);
+ } else if (from != null
+ && packet.hasChild("received", "urn:xmpp:receipts")) {
+ String id = packet.findChild("received", "urn:xmpp:receipts")
+ .getAttribute("id");
+ updateLastseen(packet, account, false);
+ mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+ id, Message.STATUS_SEND_RECEIVED);
+ } else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+ Element x = packet.findChild("x",
+ "http://jabber.org/protocol/muc#user");
+ if (x.hasChild("invite")) {
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account,
+ packet.getAttribute("from"), true);
+ if (!conversation.getMucOptions().online()) {
+ if (x.hasChild("password")) {
+ Element password = x.findChild("password");
+ conversation.getMucOptions().setPassword(
+ password.getContent());
+ mXmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ }
+ mXmppConnectionService.joinMuc(conversation);
+ mXmppConnectionService.updateConversationUi();
+ }
+ }
+ } else if (packet.hasChild("x", "jabber:x:conference")) {
+ Element x = packet.findChild("x", "jabber:x:conference");
+ String jid = x.getAttribute("jid");
+ String password = x.getAttribute("password");
+ if (jid != null) {
+ Conversation conversation = mXmppConnectionService
+ .findOrCreateConversation(account, jid, true);
+ if (!conversation.getMucOptions().online()) {
+ if (password != null) {
+ conversation.getMucOptions().setPassword(password);
+ mXmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ }
+ mXmppConnectionService.joinMuc(conversation);
+ mXmppConnectionService.updateConversationUi();
+ }
+ }
+ }
+ }
+
+ private void parseEvent(Element event, String from, Account account) {
+ Element items = event.findChild("items");
+ String node = items.getAttribute("node");
+ if (node != null) {
+ if (node.equals("urn:xmpp:avatar:metadata")) {
+ Avatar avatar = Avatar.parseMetadata(items);
+ if (avatar != null) {
+ avatar.owner = from;
+ if (mXmppConnectionService.getFileBackend().isAvatarCached(
+ avatar)) {
+ if (account.getJid().equals(from)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ mXmppConnectionService.databaseBackend
+ .updateAccount(account);
+ }
+ mXmppConnectionService.getAvatarService().clear(
+ account);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateAccountUi();
+ } else {
+ Contact contact = account.getRoster().getContact(
+ from);
+ contact.setAvatar(avatar.getFilename());
+ mXmppConnectionService.getAvatarService().clear(
+ contact);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.updateRosterUi();
+ }
+ } else {
+ mXmppConnectionService.fetchAvatar(account, avatar);
+ }
+ }
+ } else if (node.equals("http://jabber.org/protocol/nick")) {
+ Element item = items.findChild("item");
+ if (item != null) {
+ Element nick = item.findChild("nick",
+ "http://jabber.org/protocol/nick");
+ if (nick != null) {
+ if (from != null) {
+ Contact contact = account.getRoster().getContact(
+ from);
+ contact.setPresenceName(nick.getContent());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private String getPgpBody(Element message) {
+ Element child = message.findChild("x", "jabber:x:encrypted");
+ if (child == null) {
+ return null;
+ } else {
+ return child.getContent();
+ }
+ }
+
+ private boolean isMarkable(Element message) {
+ return message.hasChild("markable", "urn:xmpp:chat-markers:0");
+ }
+
+ @Override
+ public void onMessagePacketReceived(Account account, MessagePacket packet) {
+ Message message = null;
+ boolean notify = mXmppConnectionService.getPreferences().getBoolean(
+ "show_notification", true);
+ boolean alwaysNotifyInConference = notify
+ && mXmppConnectionService.getPreferences().getBoolean(
+ "always_notify_in_conference", false);
+
+ this.parseNick(packet, account);
+
+ if ((packet.getType() == MessagePacket.TYPE_CHAT || packet.getType() == MessagePacket.TYPE_NORMAL)) {
+ if ((packet.getBody() != null)
+ && (packet.getBody().startsWith("?OTR"))) {
+ message = this.parseOtrChat(packet, account);
+ if (message != null) {
+ message.markUnread();
+ }
+ } else if (packet.hasChild("body")
+ && !(packet.hasChild("x",
+ "http://jabber.org/protocol/muc#user"))) {
+ message = this.parseChat(packet, account);
+ if (message != null) {
+ message.markUnread();
+ }
+ } else if (packet.hasChild("received", "urn:xmpp:carbons:2")
+ || (packet.hasChild("sent", "urn:xmpp:carbons:2"))) {
+ message = this.parseCarbonMessage(packet, account);
+ if (message != null) {
+ if (message.getStatus() == Message.STATUS_SEND) {
+ account.activateGracePeriod();
+ notify = false;
+ mXmppConnectionService.markRead(
+ message.getConversation(), false);
+ } else {
+ message.markUnread();
+ }
+ }
+ } else {
+ parseNonMessage(packet, account);
+ }
+ } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
+ message = this.parseGroupchat(packet, account);
+ if (message != null) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ message.markUnread();
+ notify = alwaysNotifyInConference
+ || NotificationService
+ .wasHighlightedOrPrivate(message);
+ } else {
+ mXmppConnectionService.markRead(message.getConversation(),
+ false);
+ account.activateGracePeriod();
+ notify = false;
+ }
+ }
+ } else if (packet.getType() == MessagePacket.TYPE_ERROR) {
+ this.parseError(packet, account);
+ return;
+ } else if (packet.getType() == MessagePacket.TYPE_HEADLINE) {
+ this.parseHeadline(packet, account);
+ return;
+ }
+ if ((message == null) || (message.getBody() == null)) {
+ return;
+ }
+ if ((mXmppConnectionService.confirmMessages())
+ && ((packet.getId() != null))) {
+ if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
+ MessagePacket receipt = mXmppConnectionService
+ .getMessageGenerator().received(account, packet,
+ "urn:xmpp:chat-markers:0");
+ mXmppConnectionService.sendMessagePacket(account, receipt);
+ }
+ if (packet.hasChild("request", "urn:xmpp:receipts")) {
+ MessagePacket receipt = mXmppConnectionService
+ .getMessageGenerator().received(account, packet,
+ "urn:xmpp:receipts");
+ mXmppConnectionService.sendMessagePacket(account, receipt);
+ }
+ }
+ Conversation conversation = message.getConversation();
+ conversation.add(message);
+ if (packet.getType() != MessagePacket.TYPE_ERROR) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE
+ || mXmppConnectionService.saveEncryptedMessages()) {
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ }
+ }
+ if (message.bodyContainsDownloadable()) {
+ this.mXmppConnectionService.getHttpConnectionManager()
+ .createNewConnection(message);
+ }
+ notify = notify && !conversation.isMuted();
+ if (notify) {
+ mXmppConnectionService.getNotificationService().push(message);
+ }
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void parseHeadline(MessagePacket packet, Account account) {
+ if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) {
+ Element event = packet.findChild("event",
+ "http://jabber.org/protocol/pubsub#event");
+ parseEvent(event, packet.getFrom(), account);
+ }
+ }
+
+ private void parseNick(MessagePacket packet, Account account) {
+ Element nick = packet.findChild("nick",
+ "http://jabber.org/protocol/nick");
+ if (nick != null) {
+ if (packet.getFrom() != null) {
+ Contact contact = account.getRoster().getContact(
+ packet.getFrom());
+ contact.setPresenceName(nick.getContent());
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
new file mode 100644
index 00000000..4e90cda8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -0,0 +1,133 @@
+package eu.siacs.conversations.parser;
+
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public class PresenceParser extends AbstractParser implements
+ OnPresencePacketReceived {
+
+ public PresenceParser(XmppConnectionService service) {
+ super(service);
+ }
+
+ public void parseConferencePresence(PresencePacket packet, Account account) {
+ PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine();
+ if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+ Conversation muc = mXmppConnectionService.find(account, packet
+ .getAttribute("from").split("/", 2)[0]);
+ if (muc != null) {
+ boolean before = muc.getMucOptions().online();
+ muc.getMucOptions().processPacket(packet, mPgpEngine);
+ if (before != muc.getMucOptions().online()) {
+ mXmppConnectionService.updateConversationUi();
+ }
+ mXmppConnectionService.getAvatarService().clear(muc);
+ }
+ } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
+ Conversation muc = mXmppConnectionService.find(account, packet
+ .getAttribute("from").split("/", 2)[0]);
+ if (muc != null) {
+ boolean before = muc.getMucOptions().online();
+ muc.getMucOptions().processPacket(packet, mPgpEngine);
+ if (before != muc.getMucOptions().online()) {
+ mXmppConnectionService.updateConversationUi();
+ }
+ mXmppConnectionService.getAvatarService().clear(muc);
+ }
+ }
+ }
+
+ public void parseContactPresence(PresencePacket packet, Account account) {
+ PresenceGenerator mPresenceGenerator = mXmppConnectionService
+ .getPresenceGenerator();
+ if (packet.getFrom() == null) {
+ return;
+ }
+ String[] fromParts = packet.getFrom().split("/", 2);
+ String type = packet.getAttribute("type");
+ if (fromParts[0].equals(account.getJid())) {
+ if (fromParts.length == 2) {
+ if (type == null) {
+ account.updatePresence(fromParts[1],
+ Presences.parseShow(packet.findChild("show")));
+ } else if (type.equals("unavailable")) {
+ account.removePresence(fromParts[1]);
+ account.deactivateGracePeriod();
+ }
+ }
+ } else {
+ Contact contact = account.getRoster().getContact(packet.getFrom());
+ if (type == null) {
+ String presence;
+ if (fromParts.length >= 2) {
+ presence = fromParts[1];
+ } else {
+ presence = "";
+ }
+ int sizeBefore = contact.getPresences().size();
+ contact.updatePresence(presence,
+ Presences.parseShow(packet.findChild("show")));
+ PgpEngine pgp = mXmppConnectionService.getPgpEngine();
+ if (pgp != null) {
+ Element x = packet.findChild("x", "jabber:x:signed");
+ if (x != null) {
+ Element status = packet.findChild("status");
+ String msg;
+ if (status != null) {
+ msg = status.getContent();
+ } else {
+ msg = "";
+ }
+ contact.setPgpKeyId(pgp.fetchKeyId(account, msg,
+ x.getContent()));
+ }
+ }
+ boolean online = sizeBefore < contact.getPresences().size();
+ updateLastseen(packet, account, true);
+ mXmppConnectionService.onContactStatusChanged
+ .onContactStatusChanged(contact, online);
+ } else if (type.equals("unavailable")) {
+ if (fromParts.length != 2) {
+ contact.clearPresences();
+ } else {
+ contact.removePresence(fromParts[1]);
+ }
+ mXmppConnectionService.onContactStatusChanged
+ .onContactStatusChanged(contact, false);
+ } else if (type.equals("subscribe")) {
+ if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
+ mXmppConnectionService.sendPresencePacket(account,
+ mPresenceGenerator.sendPresenceUpdatesTo(contact));
+ } else {
+ contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
+ }
+ }
+ Element nick = packet.findChild("nick",
+ "http://jabber.org/protocol/nick");
+ if (nick != null) {
+ contact.setPresenceName(nick.getContent());
+ }
+ }
+ mXmppConnectionService.updateRosterUi();
+ }
+
+ @Override
+ public void onPresencePacketReceived(Account account, PresencePacket packet) {
+ if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+ this.parseConferencePresence(packet, account);
+ } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
+ this.parseConferencePresence(packet, account);
+ } else {
+ this.parseContactPresence(packet, account);
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
new file mode 100644
index 00000000..b49cf4e6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -0,0 +1,335 @@
+package eu.siacs.conversations.persistance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Roster;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class DatabaseBackend extends SQLiteOpenHelper {
+
+ private static DatabaseBackend instance = null;
+
+ private static final String DATABASE_NAME = "history";
+ private static final int DATABASE_VERSION = 8;
+
+ private static String CREATE_CONTATCS_STATEMENT = "create table "
+ + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
+ + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT,"
+ + Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
+ + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
+ + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
+ + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
+ + Account.TABLENAME + "(" + Account.UUID
+ + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ + Contact.JID + ") ON CONFLICT REPLACE);";
+
+ private DatabaseBackend(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID
+ + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT,"
+ + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT,"
+ + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS
+ + " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS
+ + " TEXT)");
+ db.execSQL("create table " + Conversation.TABLENAME + " ("
+ + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
+ + " TEXT, " + Conversation.CONTACT + " TEXT, "
+ + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID
+ + " TEXT, " + Conversation.CREATED + " NUMBER, "
+ + Conversation.STATUS + " NUMBER, " + Conversation.MODE
+ + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY("
+ + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME
+ + "(" + Account.UUID + ") ON DELETE CASCADE);");
+ db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID
+ + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, "
+ + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART
+ + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
+ + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
+ + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+ + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ + Message.CONVERSATION + ") REFERENCES "
+ + Conversation.TABLENAME + "(" + Conversation.UUID
+ + ") ON DELETE CASCADE);");
+
+ db.execSQL(CREATE_CONTATCS_STATEMENT);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 2 && newVersion >= 2) {
+ db.execSQL("update " + Account.TABLENAME + " set "
+ + Account.OPTIONS + " = " + Account.OPTIONS + " | 8");
+ }
+ if (oldVersion < 3 && newVersion >= 3) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.TYPE + " NUMBER");
+ }
+ if (oldVersion < 5 && newVersion >= 5) {
+ db.execSQL("DROP TABLE " + Contact.TABLENAME);
+ db.execSQL(CREATE_CONTATCS_STATEMENT);
+ db.execSQL("UPDATE " + Account.TABLENAME + " SET "
+ + Account.ROSTERVERSION + " = NULL");
+ }
+ if (oldVersion < 6 && newVersion >= 6) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.TRUE_COUNTERPART + " TEXT");
+ }
+ if (oldVersion < 7 && newVersion >= 7) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.REMOTE_MSG_ID + " TEXT");
+ db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ + Contact.AVATAR + " TEXT");
+ db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN "
+ + Account.AVATAR + " TEXT");
+ }
+ if (oldVersion < 8 && newVersion >= 8) {
+ db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
+ + Conversation.ATTRIBUTES + " TEXT");
+ }
+ }
+
+ public static synchronized DatabaseBackend getInstance(Context context) {
+ if (instance == null) {
+ instance = new DatabaseBackend(context);
+ }
+ return instance;
+ }
+
+ public void createConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
+ }
+
+ public void createMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Message.TABLENAME, null, message.getContentValues());
+ }
+
+ public void createAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Account.TABLENAME, null, account.getContentValues());
+ }
+
+ public void createContact(Contact contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Contact.TABLENAME, null, contact.getContentValues());
+ }
+
+ public int getConversationCount() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.rawQuery("select count(uuid) as count from "
+ + Conversation.TABLENAME + " where " + Conversation.STATUS
+ + "=" + Conversation.STATUS_AVAILABLE, null);
+ cursor.moveToFirst();
+ return cursor.getInt(0);
+ }
+
+ public CopyOnWriteArrayList<Conversation> getConversations(int status) {
+ CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<Conversation>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { Integer.toString(status) };
+ Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME
+ + " where " + Conversation.STATUS + " = ? order by "
+ + Conversation.CREATED + " desc", selectionArgs);
+ while (cursor.moveToNext()) {
+ list.add(Conversation.fromCursor(cursor));
+ }
+ return list;
+ }
+
+ public ArrayList<Message> getMessages(Conversation conversations, int limit) {
+ return getMessages(conversations, limit, -1);
+ }
+
+ public ArrayList<Message> getMessages(Conversation conversation, int limit,
+ long timestamp) {
+ ArrayList<Message> list = new ArrayList<Message>();
+ 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 + "<?", selectionArgs,
+ null, null, Message.TIME_SENT + " DESC",
+ String.valueOf(limit));
+ }
+ if (cursor.getCount() > 0) {
+ cursor.moveToLast();
+ do {
+ Message message = Message.fromCursor(cursor);
+ message.setConversation(conversation);
+ list.add(message);
+ } while (cursor.moveToPrevious());
+ }
+ return list;
+ }
+
+ public Conversation findConversation(Account account, String contactJid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { account.getUuid(), contactJid + "%" };
+ Cursor cursor = db.query(Conversation.TABLENAME, null,
+ Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID
+ + " like ?", selectionArgs, null, null, null);
+ if (cursor.getCount() == 0)
+ return null;
+ cursor.moveToFirst();
+ return Conversation.fromCursor(cursor);
+ }
+
+ public void updateConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { conversation.getUuid() };
+ db.update(Conversation.TABLENAME, conversation.getContentValues(),
+ Conversation.UUID + "=?", args);
+ }
+
+ public List<Account> getAccounts() {
+ List<Account> list = new ArrayList<Account>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.query(Account.TABLENAME, null, null, null, null,
+ null, null);
+ while (cursor.moveToNext()) {
+ list.add(Account.fromCursor(cursor));
+ }
+ cursor.close();
+ return list;
+ }
+
+ public void updateAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { account.getUuid() };
+ db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
+ + "=?", args);
+ }
+
+ public void deleteAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { account.getUuid() };
+ db.delete(Account.TABLENAME, Account.UUID + "=?", args);
+ }
+
+ public boolean hasEnabledAccounts() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from "
+ + Account.TABLENAME + " where not options & (1 <<1)", null);
+ try {
+ cursor.moveToFirst();
+ int count = cursor.getInt(0);
+ cursor.close();
+ return (count > 0);
+ } catch (SQLiteCantOpenDatabaseException e) {
+ return true; // better safe than sorry
+ }
+ }
+
+ @Override
+ public SQLiteDatabase getWritableDatabase() {
+ SQLiteDatabase db = super.getWritableDatabase();
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ return db;
+ }
+
+ public void updateMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { message.getUuid() };
+ db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
+ + "=?", args);
+ }
+
+ public void readRoster(Roster roster) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor;
+ String args[] = { roster.getAccount().getUuid() };
+ cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?",
+ args, null, null, null);
+ while (cursor.moveToNext()) {
+ roster.initContact(Contact.fromCursor(cursor));
+ }
+ cursor.close();
+ }
+
+ public void writeRoster(Roster roster) {
+ Account account = roster.getAccount();
+ SQLiteDatabase db = this.getWritableDatabase();
+ for (Contact contact : roster.getContacts()) {
+ if (contact.getOption(Contact.Options.IN_ROSTER)) {
+ db.insert(Contact.TABLENAME, null, contact.getContentValues());
+ } else {
+ String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
+ String[] whereArgs = { account.getUuid(), contact.getJid() };
+ db.delete(Contact.TABLENAME, where, whereArgs);
+ }
+ }
+ account.setRosterVersion(roster.getVersion());
+ updateAccount(account);
+ }
+
+ public void deleteMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { message.getUuid() };
+ db.delete(Message.TABLENAME, Message.UUID + "=?", args);
+ }
+
+ public void deleteMessagesInConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { conversation.getUuid() };
+ db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
+ }
+
+ public Conversation findConversationByUuid(String conversationUuid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { conversationUuid };
+ Cursor cursor = db.query(Conversation.TABLENAME, null,
+ Conversation.UUID + "=?", selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToFirst();
+ return Conversation.fromCursor(cursor);
+ }
+
+ public Message findMessageByUuid(String messageUuid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { messageUuid };
+ Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?",
+ selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToFirst();
+ return Message.fromCursor(cursor);
+ }
+
+ public Account findAccountByUuid(String accountUuid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { accountUuid };
+ Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?",
+ selectionArgs, null, null, null);
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToFirst();
+ return Account.fromCursor(cursor);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
new file mode 100644
index 00000000..b891e9ef
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -0,0 +1,480 @@
+package eu.siacs.conversations.persistance;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class FileBackend {
+
+ private static int IMAGE_SIZE = 1920;
+
+ private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
+ "yyyyMMdd_HHmmssSSS", Locale.US);
+
+ private XmppConnectionService mXmppConnectionService;
+
+ public FileBackend(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public DownloadableFile getFile(Message message) {
+ return getFile(message, true);
+ }
+
+ public DownloadableFile getFile(Message message, boolean decrypted) {
+ StringBuilder filename = new StringBuilder();
+ filename.append(getConversationsDirectory());
+ filename.append(message.getUuid());
+ if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
+ filename.append(".webp");
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ filename.append(".webp");
+ } else {
+ filename.append(".webp.pgp");
+ }
+ }
+ return new DownloadableFile(filename.toString());
+ }
+
+ public static String getConversationsDirectory() {
+ return Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PICTURES).getAbsolutePath()
+ + "/Conversations/";
+ }
+
+ public Bitmap resize(Bitmap originalBitmap, int size) {
+ int w = originalBitmap.getWidth();
+ int h = originalBitmap.getHeight();
+ if (Math.max(w, h) > size) {
+ int scalledW;
+ int scalledH;
+ if (w <= h) {
+ scalledW = (int) (w / ((double) h / size));
+ scalledH = size;
+ } else {
+ scalledW = size;
+ scalledH = (int) (h / ((double) w / size));
+ }
+ Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap,
+ scalledW, scalledH, true);
+ return scalledBitmap;
+ } else {
+ return originalBitmap;
+ }
+ }
+
+ public Bitmap rotate(Bitmap bitmap, int degree) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ Matrix mtx = new Matrix();
+ mtx.postRotate(degree);
+ return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
+ }
+
+ public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
+ throws ImageCopyException {
+ return this.copyImageToPrivateStorage(message, image, 0);
+ }
+
+ private DownloadableFile copyImageToPrivateStorage(Message message,
+ Uri image, int sampleSize) throws ImageCopyException {
+ try {
+ InputStream is = mXmppConnectionService.getContentResolver()
+ .openInputStream(image);
+ DownloadableFile file = getFile(message);
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ Bitmap originalBitmap;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int inSampleSize = (int) Math.pow(2, sampleSize);
+ Log.d(Config.LOGTAG, "reading bitmap with sample size "
+ + inSampleSize);
+ options.inSampleSize = inSampleSize;
+ originalBitmap = BitmapFactory.decodeStream(is, null, options);
+ is.close();
+ if (originalBitmap == null) {
+ throw new ImageCopyException(R.string.error_not_an_image_file);
+ }
+ Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
+ originalBitmap = null;
+ int rotation = getRotation(image);
+ if (rotation > 0) {
+ scalledBitmap = rotate(scalledBitmap, rotation);
+ }
+ OutputStream os = new FileOutputStream(file);
+ boolean success = scalledBitmap.compress(
+ Bitmap.CompressFormat.WEBP, 75, os);
+ if (!success) {
+ throw new ImageCopyException(R.string.error_compressing_image);
+ }
+ os.flush();
+ os.close();
+ long size = file.getSize();
+ int width = scalledBitmap.getWidth();
+ int height = scalledBitmap.getHeight();
+ message.setBody(Long.toString(size) + ',' + width + ',' + height);
+ return file;
+ } catch (FileNotFoundException e) {
+ throw new ImageCopyException(R.string.error_file_not_found);
+ } catch (IOException e) {
+ throw new ImageCopyException(R.string.error_io_exception);
+ } catch (SecurityException e) {
+ throw new ImageCopyException(
+ R.string.error_security_exception_during_image_copy);
+ } catch (OutOfMemoryError e) {
+ ++sampleSize;
+ if (sampleSize <= 3) {
+ return copyImageToPrivateStorage(message, image, sampleSize);
+ } else {
+ throw new ImageCopyException(R.string.error_out_of_memory);
+ }
+ }
+ }
+
+ private int getRotation(Uri image) {
+ if ("content".equals(image.getScheme())) {
+ try {
+ Cursor cursor = mXmppConnectionService
+ .getContentResolver()
+ .query(image,
+ new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
+ null, null, null);
+ if (cursor.getCount() != 1) {
+ return -1;
+ }
+ cursor.moveToFirst();
+ return cursor.getInt(0);
+ } catch (IllegalArgumentException e) {
+ return -1;
+ }
+ } else {
+ ExifInterface exif;
+ try {
+ exif = new ExifInterface(image.toString());
+ if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+ .equalsIgnoreCase("6")) {
+ return 90;
+ } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+ .equalsIgnoreCase("8")) {
+ return 270;
+ } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+ .equalsIgnoreCase("3")) {
+ return 180;
+ } else {
+ return 0;
+ }
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+ }
+
+ public Bitmap getImageFromMessage(Message message) {
+ return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
+ }
+
+ public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
+ throws FileNotFoundException {
+ Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
+ message.getUuid());
+ if ((thumbnail == null) && (!cacheOnly)) {
+ File file = getFile(message);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(file, size);
+ Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
+ options);
+ if (fullsize == null) {
+ throw new FileNotFoundException();
+ }
+ thumbnail = resize(fullsize, size);
+ this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
+ thumbnail);
+ }
+ return thumbnail;
+ }
+
+ public void removeFiles(Conversation conversation) {
+ String prefix = mXmppConnectionService.getFilesDir().getAbsolutePath();
+ String path = prefix + "/" + conversation.getAccount().getJid() + "/"
+ + conversation.getContactJid();
+ File file = new File(path);
+ try {
+ this.deleteFile(file);
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG,
+ "error deleting file: " + file.getAbsolutePath());
+ }
+ }
+
+ private void deleteFile(File f) throws IOException {
+ if (f.isDirectory()) {
+ for (File c : f.listFiles())
+ deleteFile(c);
+ }
+ f.delete();
+ }
+
+ public Uri getTakePhotoUri() {
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append(Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+ pathBuilder.append('/');
+ pathBuilder.append("Camera");
+ pathBuilder.append('/');
+ pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date())
+ + ".jpg");
+ Uri uri = Uri.parse("file://" + pathBuilder.toString());
+ File file = new File(uri.toString());
+ file.getParentFile().mkdirs();
+ return uri;
+ }
+
+ public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+ try {
+ Avatar avatar = new Avatar();
+ Bitmap bm = cropCenterSquare(image, size);
+ if (bm == null) {
+ return null;
+ }
+ ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
+ mByteArrayOutputStream, Base64.DEFAULT);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+ mBase64OutputSttream, digest);
+ if (!bm.compress(format, 75, mDigestOutputStream)) {
+ return null;
+ }
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ avatar.image = new String(mByteArrayOutputStream.toByteArray());
+ return avatar;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public boolean isAvatarCached(Avatar avatar) {
+ File file = new File(getAvatarPath(avatar.getFilename()));
+ return file.exists();
+ }
+
+ public boolean save(Avatar avatar) {
+ if (isAvatarCached(avatar)) {
+ return true;
+ }
+ String filename = getAvatarPath(avatar.getFilename());
+ File file = new File(filename + ".tmp");
+ file.getParentFile().mkdirs();
+ try {
+ file.createNewFile();
+ FileOutputStream mFileOutputStream = new FileOutputStream(file);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+ mFileOutputStream, digest);
+ mDigestOutputStream.write(avatar.getImageAsBytes());
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ avatar.size = file.length();
+ String sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ if (sha1sum.equals(avatar.sha1sum)) {
+ file.renameTo(new File(filename));
+ return true;
+ } else {
+ Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
+ file.delete();
+ return false;
+ }
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ } catch (NoSuchAlgorithmException e) {
+ return false;
+ }
+ }
+
+ public String getAvatarPath(String avatar) {
+ return mXmppConnectionService.getFilesDir().getAbsolutePath()
+ + "/avatars/" + avatar;
+ }
+
+ public Uri getAvatarUri(String avatar) {
+ return Uri.parse("file:" + getAvatarPath(avatar));
+ }
+
+ public Bitmap cropCenterSquare(Uri image, int size) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image, size);
+ InputStream is = mXmppConnectionService.getContentResolver()
+ .openInputStream(image);
+ Bitmap input = BitmapFactory.decodeStream(is, null, options);
+ if (input == null) {
+ return null;
+ } else {
+ int rotation = getRotation(image);
+ if (rotation > 0) {
+ input = rotate(input, rotation);
+ }
+ return cropCenterSquare(input, size);
+ }
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image,
+ Math.max(newHeight, newWidth));
+ InputStream is = mXmppConnectionService.getContentResolver()
+ .openInputStream(image);
+ Bitmap source = BitmapFactory.decodeStream(is, null, options);
+
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+ float xScale = (float) newWidth / sourceWidth;
+ float yScale = (float) newHeight / sourceHeight;
+ float scale = Math.max(xScale, yScale);
+ float scaledWidth = scale * sourceWidth;
+ float scaledHeight = scale * sourceHeight;
+ float left = (newWidth - scaledWidth) / 2;
+ float top = (newHeight - scaledHeight) / 2;
+
+ RectF targetRect = new RectF(left, top, left + scaledWidth, top
+ + scaledHeight);
+ Bitmap dest = Bitmap.createBitmap(newWidth, newHeight,
+ source.getConfig());
+ Canvas canvas = new Canvas(dest);
+ canvas.drawBitmap(source, null, targetRect, null);
+
+ return dest;
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+
+ }
+
+ public Bitmap cropCenterSquare(Bitmap input, int size) {
+ int w = input.getWidth();
+ int h = input.getHeight();
+
+ float scale = Math.max((float) size / h, (float) size / w);
+
+ float outWidth = scale * w;
+ float outHeight = scale * h;
+ float left = (size - outWidth) / 2;
+ float top = (size - outHeight) / 2;
+ RectF target = new RectF(left, top, left + outWidth, top + outHeight);
+
+ Bitmap output = Bitmap.createBitmap(size, size, input.getConfig());
+ Canvas canvas = new Canvas(output);
+ canvas.drawBitmap(input, null, target, null);
+ return output;
+ }
+
+ private int calcSampleSize(Uri image, int size)
+ throws FileNotFoundException {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver()
+ .openInputStream(image), null, options);
+ return calcSampleSize(options, size);
+ }
+
+ private int calcSampleSize(File image, int size) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(image.getAbsolutePath(), options);
+ return calcSampleSize(options, size);
+ }
+
+ private int calcSampleSize(BitmapFactory.Options options, int size) {
+ int height = options.outHeight;
+ int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > size || width > size) {
+ int halfHeight = height / 2;
+ int halfWidth = width / 2;
+
+ while ((halfHeight / inSampleSize) > size
+ && (halfWidth / inSampleSize) > size) {
+ inSampleSize *= 2;
+ }
+ }
+ return inSampleSize;
+ }
+
+ public Uri getJingleFileUri(Message message) {
+ File file = getFile(message);
+ return Uri.parse("file://" + file.getAbsolutePath());
+ }
+
+ public class ImageCopyException extends Exception {
+ private static final long serialVersionUID = -1010013599132881427L;
+ private int resId;
+
+ public ImageCopyException(int resId) {
+ this.resId = resId;
+ }
+
+ public int getResId() {
+ return resId;
+ }
+ }
+
+ public Bitmap getAvatar(String avatar, int size) {
+ if (avatar == null) {
+ return null;
+ }
+ Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
+ if (bm == null) {
+ return null;
+ }
+ return bm;
+ }
+
+ public boolean isFileAvailable(Message message) {
+ return getFile(message).exists();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java
new file mode 100644
index 00000000..6a457b17
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.persistance;
+
+public interface OnPhoneContactsMerged {
+ public void phoneContactsMerged();
+}
diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
new file mode 100644
index 00000000..676a09c9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
@@ -0,0 +1,23 @@
+package eu.siacs.conversations.services;
+
+public class AbstractConnectionManager {
+ protected XmppConnectionService mXmppConnectionService;
+
+ public AbstractConnectionManager(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public XmppConnectionService getXmppConnectionService() {
+ return this.mXmppConnectionService;
+ }
+
+ public long getAutoAcceptFileSize() {
+ String config = this.mXmppConnectionService.getPreferences().getString(
+ "auto_accept_file_size", "524288");
+ try {
+ return Long.parseLong(config);
+ } catch (NumberFormatException e) {
+ return 524288;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java
new file mode 100644
index 00000000..c0668a19
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -0,0 +1,298 @@
+package eu.siacs.conversations.services;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.entities.MucOptions;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.util.Log;
+
+public class AvatarService {
+
+ private static final int FG_COLOR = 0xFFFAFAFA;
+ private static final int TRANSPARENT = 0x00000000;
+
+ private static final String PREFIX_CONTACT = "contact";
+ private static final String PREFIX_CONVERSATION = "conversation";
+ private static final String PREFIX_ACCOUNT = "account";
+ private static final String PREFIX_GENERIC = "generic";
+
+ private ArrayList<Integer> sizes = new ArrayList<Integer>();
+
+ protected XmppConnectionService mXmppConnectionService = null;
+
+ public AvatarService(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public Bitmap get(Contact contact, int size) {
+ final String KEY = key(contact, size);
+ Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
+ if (avatar != null) {
+ return avatar;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(
+ contact.getAvatar(), size);
+ if (avatar == null) {
+ if (contact.getProfilePhoto() != null) {
+ avatar = mXmppConnectionService.getFileBackend()
+ .cropCenterSquare(Uri.parse(contact.getProfilePhoto()),
+ size);
+ if (avatar == null) {
+ avatar = get(contact.getDisplayName(), size);
+ }
+ } else {
+ avatar = get(contact.getDisplayName(), size);
+ }
+ }
+ this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+ return avatar;
+ }
+
+ public void clear(Contact contact) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ key(contact, size));
+ }
+ }
+
+ private String key(Contact contact, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_CONTACT + "_" + contact.getAccount().getJid() + "_"
+ + contact.getJid() + "_" + String.valueOf(size);
+ }
+
+ public Bitmap get(ListItem item, int size) {
+ if (item instanceof Contact) {
+ return get((Contact) item, size);
+ } else if (item instanceof Bookmark) {
+ Bookmark bookmark = (Bookmark) item;
+ if (bookmark.getConversation() != null) {
+ return get(bookmark.getConversation(), size);
+ } else {
+ return get(bookmark.getDisplayName(), size);
+ }
+ } else {
+ return get(item.getDisplayName(), size);
+ }
+ }
+
+ public Bitmap get(Conversation conversation, int size) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ return get(conversation.getContact(), size);
+ } else {
+ return get(conversation.getMucOptions(), size);
+ }
+ }
+
+ public void clear(Conversation conversation) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ clear(conversation.getContact());
+ } else {
+ clear(conversation.getMucOptions());
+ }
+ }
+
+ public Bitmap get(MucOptions mucOptions, int size) {
+ final String KEY = key(mucOptions, size);
+ Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ List<MucOptions.User> users = mucOptions.getUsers();
+ int count = users.size();
+ bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ bitmap.eraseColor(TRANSPARENT);
+
+ if (count == 0) {
+ String name = mucOptions.getConversation().getName();
+ String letter = name.substring(0, 1);
+ int color = this.getColorForName(name);
+ drawTile(canvas, letter, color, 0, 0, size, size);
+ } else if (count == 1) {
+ drawTile(canvas, users.get(0), 0, 0, size, size);
+ } else if (count == 2) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+ drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
+ } else if (count == 3) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+ drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size,
+ size);
+ } else if (count == 4) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+ drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+ drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size,
+ size);
+ } else {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+ drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+ drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, "\u2026", 0xFF202020, size / 2 + 1, size / 2 + 1,
+ size, size);
+ }
+ this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+ return bitmap;
+ }
+
+ public void clear(MucOptions options) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ key(options, size));
+ }
+ }
+
+ private String key(MucOptions options, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid()
+ + "_" + String.valueOf(size);
+ }
+
+ public Bitmap get(Account account, int size) {
+ final String KEY = key(account, size);
+ Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
+ if (avatar != null) {
+ return avatar;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(
+ account.getAvatar(), size);
+ if (avatar == null) {
+ avatar = get(account.getJid(), size);
+ }
+ mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+ return avatar;
+ }
+
+ public void clear(Account account) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ key(account, size));
+ }
+ }
+
+ private String key(Account account, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_ACCOUNT + "_" + account.getUuid() + "_"
+ + String.valueOf(size);
+ }
+
+ public Bitmap get(String name, int size) {
+ final String KEY = key(name, size);
+ Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+ bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ String letter = name.substring(0, 1);
+ int color = this.getColorForName(name);
+ drawTile(canvas, letter, color, 0, 0, size, size);
+ mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+ return bitmap;
+ }
+
+ private String key(String name, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
+ }
+
+ private void drawTile(Canvas canvas, String letter, int tileColor,
+ int left, int top, int right, int bottom) {
+ letter = letter.toUpperCase(Locale.getDefault());
+ Paint tilePaint = new Paint(), textPaint = new Paint();
+ tilePaint.setColor(tileColor);
+ textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
+ textPaint.setColor(FG_COLOR);
+ textPaint.setTypeface(Typeface.create("sans-serif-light",
+ Typeface.NORMAL));
+ textPaint.setTextSize((float) ((right - left) * 0.8));
+ Rect rect = new Rect();
+
+ canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
+ textPaint.getTextBounds(letter, 0, 1, rect);
+ float width = textPaint.measureText(letter);
+ canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
+ / 2 + rect.height() / 2, textPaint);
+ }
+
+ private void drawTile(Canvas canvas, MucOptions.User user, int left,
+ int top, int right, int bottom) {
+ Contact contact = user.getContact();
+ if (contact != null) {
+ Uri uri = null;
+ if (contact.getAvatar() != null) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(
+ contact.getAvatar());
+ } else if (contact.getProfilePhoto() != null) {
+ uri = Uri.parse(contact.getProfilePhoto());
+ }
+ if (uri != null) {
+ Bitmap bitmap = mXmppConnectionService.getFileBackend()
+ .cropCenter(uri, bottom - top, right - left);
+ if (bitmap != null) {
+ drawTile(canvas, bitmap, left, top, right, bottom);
+ } else {
+ String letter = user.getName().substring(0, 1);
+ int color = this.getColorForName(user.getName());
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ }
+ } else {
+ String letter = user.getName().substring(0, 1);
+ int color = this.getColorForName(user.getName());
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ }
+ } else {
+ String letter = user.getName().substring(0, 1);
+ int color = this.getColorForName(user.getName());
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ }
+ }
+
+ private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop,
+ int dstright, int dstbottom) {
+ Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
+ canvas.drawBitmap(bm, null, dst, null);
+ }
+
+ private int getColorForName(String name) {
+ int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
+ 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
+ 0xFF795548, 0xFF607d8b };
+ return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)];
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
new file mode 100644
index 00000000..dfbe9db7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
@@ -0,0 +1,24 @@
+package eu.siacs.conversations.services;
+
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class EventReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Intent mIntentForService = new Intent(context,
+ XmppConnectionService.class);
+ if (intent.getAction() != null) {
+ mIntentForService.setAction(intent.getAction());
+ } else {
+ mIntentForService.setAction("other");
+ }
+ if (intent.getAction().equals("ui")
+ || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
+ context.startService(mIntentForService);
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
new file mode 100644
index 00000000..00765deb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -0,0 +1,237 @@
+package eu.siacs.conversations.services;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.Html;
+import android.util.DisplayMetrics;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+
+public class NotificationService {
+
+ private XmppConnectionService mXmppConnectionService;
+
+ private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>();
+
+ public int NOTIFICATION_ID = 0x2342;
+ private Conversation mOpenConversation;
+ private boolean mIsInForeground;
+
+ public NotificationService(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public void push(Message message) {
+ PowerManager pm = (PowerManager) mXmppConnectionService
+ .getSystemService(Context.POWER_SERVICE);
+ boolean isScreenOn = pm.isScreenOn();
+
+ if (this.mIsInForeground && isScreenOn
+ && this.mOpenConversation == message.getConversation()) {
+ return;
+ }
+ synchronized (notifications) {
+ String conversationUuid = message.getConversationUuid();
+ if (notifications.containsKey(conversationUuid)) {
+ notifications.get(conversationUuid).add(message);
+ } else {
+ ArrayList<Message> mList = new ArrayList<Message>();
+ mList.add(message);
+ notifications.put(conversationUuid, mList);
+ }
+ Account account = message.getConversation().getAccount();
+ updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
+ && !account.inGracePeriod());
+ }
+
+ }
+
+ public void clear() {
+ synchronized (notifications) {
+ notifications.clear();
+ updateNotification(false);
+ }
+ }
+
+ public void clear(Conversation conversation) {
+ synchronized (notifications) {
+ notifications.remove(conversation.getUuid());
+ updateNotification(false);
+ }
+ }
+
+ private void updateNotification(boolean notify) {
+ NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ SharedPreferences preferences = mXmppConnectionService.getPreferences();
+
+ String ringtone = preferences.getString("notification_ringtone", null);
+ boolean vibrate = preferences.getBoolean("vibrate_on_notification",
+ true);
+
+ if (notifications.size() == 0) {
+ notificationManager.cancel(NOTIFICATION_ID);
+ } else {
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ mBuilder.setSmallIcon(R.drawable.ic_notification);
+ if (notifications.size() == 1) {
+ ArrayList<Message> messages = notifications.values().iterator()
+ .next();
+ if (messages.size() >= 1) {
+ Conversation conversation = messages.get(0)
+ .getConversation();
+ mBuilder.setLargeIcon(mXmppConnectionService
+ .getAvatarService().get(conversation, getPixel(64)));
+ mBuilder.setContentTitle(conversation.getName());
+ StringBuilder text = new StringBuilder();
+ for (int i = 0; i < messages.size(); ++i) {
+ text.append(messages.get(i).getReadableBody(
+ mXmppConnectionService));
+ if (i != messages.size() - 1) {
+ text.append("\n");
+ }
+ }
+ mBuilder.setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(text.toString()));
+ mBuilder.setContentText(messages.get(0).getReadableBody(
+ mXmppConnectionService));
+ if (notify) {
+ mBuilder.setTicker(messages.get(messages.size() - 1)
+ .getReadableBody(mXmppConnectionService));
+ }
+ mBuilder.setContentIntent(createContentIntent(conversation
+ .getUuid()));
+ } else {
+ notificationManager.cancel(NOTIFICATION_ID);
+ return;
+ }
+ } else {
+ NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+ style.setBigContentTitle(notifications.size()
+ + " "
+ + mXmppConnectionService
+ .getString(R.string.unread_conversations));
+ StringBuilder names = new StringBuilder();
+ Conversation conversation = null;
+ for (ArrayList<Message> messages : notifications.values()) {
+ if (messages.size() > 0) {
+ conversation = messages.get(0).getConversation();
+ String name = conversation.getName();
+ style.addLine(Html.fromHtml("<b>"
+ + name
+ + "</b> "
+ + messages.get(0).getReadableBody(
+ mXmppConnectionService)));
+ names.append(name);
+ names.append(", ");
+ }
+ }
+ if (names.length() >= 2) {
+ names.delete(names.length() - 2, names.length());
+ }
+ mBuilder.setContentTitle(notifications.size()
+ + " "
+ + mXmppConnectionService
+ .getString(R.string.unread_conversations));
+ mBuilder.setContentText(names.toString());
+ mBuilder.setStyle(style);
+ if (conversation != null) {
+ mBuilder.setContentIntent(createContentIntent(conversation
+ .getUuid()));
+ }
+ }
+ if (notify) {
+ if (vibrate) {
+ int dat = 70;
+ long[] pattern = { 0, 3 * dat, dat, dat };
+ mBuilder.setVibrate(pattern);
+ }
+ if (ringtone != null) {
+ mBuilder.setSound(Uri.parse(ringtone));
+ }
+ }
+ mBuilder.setDeleteIntent(createDeleteIntent());
+ mBuilder.setLights(0xffffffff, 2000, 4000);
+ Notification notification = mBuilder.build();
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ }
+
+ private PendingIntent createContentIntent(String conversationUuid) {
+ TaskStackBuilder stackBuilder = TaskStackBuilder
+ .create(mXmppConnectionService);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ Intent viewConversationIntent = new Intent(mXmppConnectionService,
+ ConversationActivity.class);
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+ conversationUuid);
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+
+ stackBuilder.addNextIntent(viewConversationIntent);
+
+ PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ return resultPendingIntent;
+ }
+
+ private PendingIntent createDeleteIntent() {
+ Intent intent = new Intent(mXmppConnectionService,
+ XmppConnectionService.class);
+ intent.setAction("clear_notification");
+ return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
+ }
+
+ public static boolean wasHighlightedOrPrivate(Message message) {
+ String nick = message.getConversation().getMucOptions().getActualNick();
+ Pattern highlight = generateNickHighlightPattern(nick);
+ if (message.getBody() == null || nick == null) {
+ return false;
+ }
+ Matcher m = highlight.matcher(message.getBody());
+ return (m.find() || message.getType() == Message.TYPE_PRIVATE);
+ }
+
+ private static Pattern generateNickHighlightPattern(String nick) {
+ // We expect a word boundary, i.e. space or start of string, followed by
+ // the
+ // nick (matched in case-insensitive manner), followed by optional
+ // punctuation (for example "bob: i disagree" or "how are you alice?"),
+ // followed by another word boundary.
+ return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b",
+ Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+ }
+
+ public void setOpenConversation(Conversation conversation) {
+ this.mOpenConversation = conversation;
+ }
+
+ public void setIsInForeground(boolean foreground) {
+ this.mIsInForeground = foreground;
+ }
+
+ private int getPixel(int dp) {
+ DisplayMetrics metrics = mXmppConnectionService.getResources()
+ .getDisplayMetrics();
+ return ((int) (dp * metrics.density));
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
new file mode 100644
index 00000000..37e334eb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -0,0 +1,1927 @@
+package eu.siacs.conversations.services;
+
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.generator.IqGenerator;
+import eu.siacs.conversations.generator.MessageGenerator;
+import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.parser.MessageParser;
+import eu.siacs.conversations.parser.PresenceParser;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
+import eu.siacs.conversations.utils.PRNGFixes;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnBindListener;
+import eu.siacs.conversations.xmpp.OnContactStatusChanged;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
+import eu.siacs.conversations.xmpp.OnStatusChanged;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
+import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.FileObserver;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+import android.util.LruCache;
+
+public class XmppConnectionService extends Service {
+
+ public DatabaseBackend databaseBackend;
+ private FileBackend fileBackend = new FileBackend(this);
+
+ public long startDate;
+
+ private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
+ public static String ACTION_CLEAR_NOTIFICATION = "clear_notification";
+
+ private MemorizingTrustManager mMemorizingTrustManager;
+
+ private NotificationService mNotificationService = new NotificationService(
+ this);
+
+ private MessageParser mMessageParser = new MessageParser(this);
+ private PresenceParser mPresenceParser = new PresenceParser(this);
+ private IqParser mIqParser = new IqParser(this);
+ private MessageGenerator mMessageGenerator = new MessageGenerator(this);
+ private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
+
+ private List<Account> accounts;
+ private CopyOnWriteArrayList<Conversation> conversations = null;
+ private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
+ this);
+ private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
+ this);
+ private AvatarService mAvatarService = new AvatarService(this);
+
+ private OnConversationUpdate mOnConversationUpdate = null;
+ private Integer convChangedListenerCount = 0;
+ private OnAccountUpdate mOnAccountUpdate = null;
+ private Integer accountChangedListenerCount = 0;
+ private OnRosterUpdate mOnRosterUpdate = null;
+ private Integer rosterChangedListenerCount = 0;
+ public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
+
+ @Override
+ public void onContactStatusChanged(Contact contact, boolean online) {
+ Conversation conversation = find(getConversations(), contact);
+ if (conversation != null) {
+ conversation.endOtrIfNeeded();
+ if (online && (contact.getPresences().size() == 1)) {
+ sendUnsendMessages(conversation);
+ }
+ }
+ }
+ };
+
+ private SecureRandom mRandom;
+
+ private ContentObserver contactObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ Intent intent = new Intent(getApplicationContext(),
+ XmppConnectionService.class);
+ intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
+ startService(intent);
+ }
+ };
+
+ private FileObserver fileObserver = new FileObserver(
+ FileBackend.getConversationsDirectory()) {
+
+ @Override
+ public void onEvent(int event, String path) {
+ if (event == FileObserver.DELETE) {
+ markFileDeleted(path.split("\\.")[0]);
+ }
+ }
+ };
+
+ private final IBinder mBinder = new XmppConnectionBinder();
+ private OnStatusChanged statusListener = new OnStatusChanged() {
+
+ @Override
+ public void onStatusChanged(Account account) {
+ XmppConnection connection = account.getXmppConnection();
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ ;
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ for (Conversation conversation : account.pendingConferenceLeaves) {
+ leaveMuc(conversation);
+ }
+ for (Conversation conversation : account.pendingConferenceJoins) {
+ joinMuc(conversation);
+ }
+ mJingleConnectionManager.cancelInTransmission();
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); ++i) {
+ if (conversations.get(i).getAccount() == account) {
+ conversations.get(i).startOtrIfNeeded();
+ sendUnsendMessages(conversations.get(i));
+ }
+ }
+ if (connection != null && connection.getFeatures().csi()) {
+ if (checkListeners()) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + " sending csi//inactive");
+ connection.sendInactive();
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + " sending csi//active");
+ connection.sendActive();
+ }
+ }
+ syncDirtyContacts(account);
+ scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
+ } else if (account.getStatus() == Account.STATUS_OFFLINE) {
+ resetSendingToWaiting(account);
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ int timeToReconnect = mRandom.nextInt(50) + 10;
+ scheduleWakeupCall(timeToReconnect, false);
+ }
+ } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
+ databaseBackend.updateAccount(account);
+ reconnectAccount(account, true);
+ } else if ((account.getStatus() != Account.STATUS_CONNECTING)
+ && (account.getStatus() != Account.STATUS_NO_INTERNET)) {
+ if (connection != null) {
+ int next = connection.getTimeToNextAttempt();
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": error connecting account. try again in "
+ + next + "s for the "
+ + (connection.getAttempt() + 1) + " time");
+ scheduleWakeupCall((int) (next * 1.2), false);
+ }
+ }
+ UIHelper.showErrorNotification(getApplicationContext(),
+ getAccounts());
+ }
+ };
+
+ private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
+
+ @Override
+ public void onJinglePacketReceived(Account account, JinglePacket packet) {
+ mJingleConnectionManager.deliverPacket(account, packet);
+ }
+ };
+
+ private OpenPgpServiceConnection pgpServiceConnection;
+ private PgpEngine mPgpEngine = null;
+ private Intent pingIntent;
+ private PendingIntent pendingPingIntent = null;
+ private WakeLock wakeLock;
+ private PowerManager pm;
+ private OnBindListener mOnBindListener = new OnBindListener() {
+
+ @Override
+ public void onBind(final Account account) {
+ account.getRoster().clearPresences();
+ account.clearPresences(); // self presences
+ account.pendingConferenceJoins.clear();
+ account.pendingConferenceLeaves.clear();
+ fetchRosterFromServer(account);
+ fetchBookmarks(account);
+ sendPresencePacket(account,
+ mPresenceGenerator.sendPresence(account));
+ connectMultiModeConversations(account);
+ updateConversationUi();
+ }
+ };
+
+ private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
+
+ @Override
+ public void onMessageAcknowledged(Account account, String uuid) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ for (Message message : conversation.getMessages()) {
+ if ((message.getStatus() == Message.STATUS_UNSEND || message
+ .getStatus() == Message.STATUS_WAITING)
+ && message.getUuid().equals(uuid)) {
+ markMessage(message, Message.STATUS_SEND);
+ return;
+ }
+ }
+ }
+ }
+ }
+ };
+ private LruCache<String, Bitmap> mBitmapCache;
+
+ public PgpEngine getPgpEngine() {
+ if (pgpServiceConnection.isBound()) {
+ if (this.mPgpEngine == null) {
+ this.mPgpEngine = new PgpEngine(new OpenPgpApi(
+ getApplicationContext(),
+ pgpServiceConnection.getService()), this);
+ }
+ return mPgpEngine;
+ } else {
+ return null;
+ }
+
+ }
+
+ public FileBackend getFileBackend() {
+ return this.fileBackend;
+ }
+
+ public AvatarService getAvatarService() {
+ return this.mAvatarService;
+ }
+
+ public Message attachImageToConversation(final Conversation conversation,
+ final Uri uri, final UiCallback<Message> callback) {
+ final Message message;
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "",
+ Message.ENCRYPTION_DECRYPTED);
+ } else {
+ message = new Message(conversation, "",
+ conversation.getNextEncryption(forceEncryption()));
+ }
+ message.setPresence(conversation.getNextPresence());
+ message.setType(Message.TYPE_IMAGE);
+ message.setStatus(Message.STATUS_OFFERED);
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ getFileBackend().copyImageToPrivateStorage(message, uri);
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ getPgpEngine().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (FileBackend.ImageCopyException e) {
+ callback.error(e.getResId(), message);
+ }
+ }
+ }).start();
+ return message;
+ }
+
+ public Conversation find(Bookmark bookmark) {
+ return find(bookmark.getAccount(), bookmark.getJid());
+ }
+
+ public Conversation find(Account account, String jid) {
+ return find(getConversations(), account, jid);
+ }
+
+ public class XmppConnectionBinder extends Binder {
+ public XmppConnectionService getService() {
+ return XmppConnectionService.this;
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.getAction() != null) {
+ if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) {
+ mergePhoneContactsWithRoster();
+ return START_STICKY;
+ } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) {
+ logoutAndSave();
+ return START_NOT_STICKY;
+ } else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) {
+ mNotificationService.clear();
+ }
+ }
+ this.wakeLock.acquire();
+
+ for (Account account : accounts) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (!hasInternetConnection()) {
+ account.setStatus(Account.STATUS_NO_INTERNET);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ } else {
+ if (account.getStatus() == Account.STATUS_NO_INTERNET) {
+ account.setStatus(Account.STATUS_OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ long lastReceived = account.getXmppConnection()
+ .getLastPacketReceived();
+ long lastSent = account.getXmppConnection()
+ .getLastPingSent();
+ if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": ping timeout");
+ this.reconnectAccount(account, true);
+ } else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) {
+ account.getXmppConnection().sendPing();
+ this.scheduleWakeupCall(2, false);
+ }
+ } else if (account.getStatus() == Account.STATUS_OFFLINE) {
+ if (account.getXmppConnection() == null) {
+ account.setXmppConnection(this
+ .createConnection(account));
+ }
+ new Thread(account.getXmppConnection()).start();
+ } else if ((account.getStatus() == Account.STATUS_CONNECTING)
+ && ((SystemClock.elapsedRealtime() - account
+ .getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": time out during connect reconnecting");
+ reconnectAccount(account, true);
+ } else {
+ if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
+ reconnectAccount(account, true);
+ }
+ }
+ // in any case. reschedule wakup call
+ this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
+ }
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ }
+ }
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return START_STICKY;
+ }
+
+ public boolean hasInternetConnection() {
+ ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ return activeNetwork != null && activeNetwork.isConnected();
+ }
+
+ @SuppressLint("TrulyRandom")
+ @Override
+ public void onCreate() {
+ ExceptionHelper.init(getApplicationContext());
+ PRNGFixes.apply();
+ this.mRandom = new SecureRandom();
+ this.mMemorizingTrustManager = new MemorizingTrustManager(
+ getApplicationContext());
+
+ int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ int cacheSize = maxMemory / 8;
+ this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
+ @Override
+ protected int sizeOf(String key, Bitmap bitmap) {
+ return bitmap.getByteCount() / 1024;
+ }
+ };
+
+ this.databaseBackend = DatabaseBackend
+ .getInstance(getApplicationContext());
+ this.accounts = databaseBackend.getAccounts();
+
+ for (Account account : this.accounts) {
+ this.databaseBackend.readRoster(account.getRoster());
+ }
+ this.mergePhoneContactsWithRoster();
+ this.getConversations();
+
+ getContentResolver().registerContentObserver(
+ ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+ this.fileObserver.startWatching();
+ this.pgpServiceConnection = new OpenPgpServiceConnection(
+ getApplicationContext(), "org.sufficientlysecure.keychain");
+ this.pgpServiceConnection.bindToService();
+
+ this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "XmppConnectionService");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ this.logoutAndSave();
+ }
+
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ super.onTaskRemoved(rootIntent);
+ this.logoutAndSave();
+ }
+
+ private void logoutAndSave() {
+ for (Account account : accounts) {
+ databaseBackend.writeRoster(account.getRoster());
+ if (account.getXmppConnection() != null) {
+ disconnect(account, false);
+ }
+ }
+ Context context = getApplicationContext();
+ AlarmManager alarmManager = (AlarmManager) context
+ .getSystemService(Context.ALARM_SERVICE);
+ Intent intent = new Intent(context, EventReceiver.class);
+ alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
+ Log.d(Config.LOGTAG, "good bye");
+ stopSelf();
+ }
+
+ protected void scheduleWakeupCall(int seconds, boolean ping) {
+ long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
+ Context context = getApplicationContext();
+ AlarmManager alarmManager = (AlarmManager) context
+ .getSystemService(Context.ALARM_SERVICE);
+
+ if (ping) {
+ if (this.pingIntent == null) {
+ this.pingIntent = new Intent(context, EventReceiver.class);
+ this.pingIntent.setAction("ping");
+ this.pingIntent.putExtra("time", timeToWake);
+ this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
+ this.pingIntent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ timeToWake, pendingPingIntent);
+ } else {
+ long scheduledTime = this.pingIntent.getLongExtra("time", 0);
+ if (scheduledTime < SystemClock.elapsedRealtime()
+ || (scheduledTime > timeToWake)) {
+ this.pingIntent.putExtra("time", timeToWake);
+ alarmManager.cancel(this.pendingPingIntent);
+ this.pendingPingIntent = PendingIntent.getBroadcast(
+ context, 0, this.pingIntent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ timeToWake, pendingPingIntent);
+ }
+ }
+ } else {
+ Intent intent = new Intent(context, EventReceiver.class);
+ intent.setAction("ping_check");
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
+ intent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
+ alarmIntent);
+ }
+
+ }
+
+ public XmppConnection createConnection(Account account) {
+ SharedPreferences sharedPref = getPreferences();
+ account.setResource(sharedPref.getString("resource", "mobile")
+ .toLowerCase(Locale.getDefault()));
+ XmppConnection connection = new XmppConnection(account, this);
+ connection.setOnMessagePacketReceivedListener(this.mMessageParser);
+ connection.setOnStatusChangedListener(this.statusListener);
+ connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
+ connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
+ connection.setOnJinglePacketReceivedListener(this.jingleListener);
+ connection.setOnBindListener(this.mOnBindListener);
+ connection
+ .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
+ return connection;
+ }
+
+ public void sendMessage(Message message) {
+ Account account = message.getConversation().getAccount();
+ account.deactivateGracePeriod();
+ Conversation conv = message.getConversation();
+ MessagePacket packet = null;
+ boolean saveInDb = true;
+ boolean send = false;
+ if (account.getStatus() == Account.STATUS_ONLINE
+ && account.getXmppConnection() != null) {
+ if (message.getType() == Message.TYPE_IMAGE) {
+ if (message.getPresence() != null) {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()
+ && (message.getPresence() != null)) {
+ conv.startOtrSession(this, message.getPresence(),
+ true);
+ message.setStatus(Message.STATUS_WAITING);
+ } else if (conv.hasValidOtrSession()
+ && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+ mJingleConnectionManager
+ .createNewConnection(message);
+ } else if (message.getPresence() == null) {
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ } else {
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()
+ && (message.getPresence() != null)) {
+ conv.startOtrSession(this, message.getPresence(), true);
+ message.setStatus(Message.STATUS_WAITING);
+ } else if (conv.hasValidOtrSession()
+ && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+ message.setPresence(conv.getOtrSession().getSessionID()
+ .getUserID());
+ packet = mMessageGenerator.generateOtrChat(message);
+ send = true;
+
+ } else if (message.getPresence() == null) {
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ message.getConversation().endOtrIfNeeded();
+ failWaitingOtrMessages(message.getConversation());
+ packet = mMessageGenerator.generatePgpChat(message);
+ send = true;
+ } else {
+ message.getConversation().endOtrIfNeeded();
+ failWaitingOtrMessages(message.getConversation());
+ packet = mMessageGenerator.generateChat(message);
+ send = true;
+ }
+ }
+ if (!account.getXmppConnection().getFeatures().sm()
+ && conv.getMode() != Conversation.MODE_MULTI) {
+ message.setStatus(Message.STATUS_SEND);
+ }
+ } else {
+ message.setStatus(Message.STATUS_WAITING);
+ if (message.getType() == Message.TYPE_TEXT) {
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ String pgpBody = message.getEncryptedBody();
+ String decryptedBody = message.getBody();
+ message.setBody(pgpBody);
+ message.setEncryption(Message.ENCRYPTION_PGP);
+ databaseBackend.createMessage(message);
+ saveInDb = false;
+ message.setBody(decryptedBody);
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (conv.hasValidOtrSession()) {
+ message.setPresence(conv.getOtrSession().getSessionID()
+ .getUserID());
+ } else if (!conv.hasValidOtrSession()
+ && message.getPresence() != null) {
+ conv.startOtrSession(this, message.getPresence(), false);
+ }
+ }
+ }
+
+ }
+ conv.add(message);
+ if (saveInDb) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE
+ || saveEncryptedMessages()) {
+ databaseBackend.createMessage(message);
+ }
+ }
+ if ((send) && (packet != null)) {
+ sendMessagePacket(account, packet);
+ }
+ updateConversationUi();
+ }
+
+ private void sendUnsendMessages(Conversation conversation) {
+ for (int i = 0; i < conversation.getMessages().size(); ++i) {
+ int status = conversation.getMessages().get(i).getStatus();
+ if (status == Message.STATUS_WAITING) {
+ resendMessage(conversation.getMessages().get(i));
+ }
+ }
+ }
+
+ private void resendMessage(Message message) {
+ Account account = message.getConversation().getAccount();
+ MessagePacket packet = null;
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Presences presences = message.getConversation().getContact()
+ .getPresences();
+ if (!message.getConversation().hasValidOtrSession()) {
+ if ((message.getPresence() != null)
+ && (presences.has(message.getPresence()))) {
+ message.getConversation().startOtrSession(this,
+ message.getPresence(), true);
+ } else {
+ if (presences.size() == 1) {
+ String presence = presences.asStringArray()[0];
+ message.getConversation().startOtrSession(this,
+ presence, true);
+ }
+ }
+ } else {
+ if (message.getConversation().getOtrSession()
+ .getSessionStatus() == SessionStatus.ENCRYPTED) {
+ if (message.getType() == Message.TYPE_TEXT) {
+ packet = mMessageGenerator.generateOtrChat(message,
+ true);
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ }
+ }
+ } else if (message.getType() == Message.TYPE_TEXT) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+ packet = mMessageGenerator.generateChat(message, true);
+ } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
+ || (message.getEncryption() == Message.ENCRYPTION_PGP)) {
+ packet = mMessageGenerator.generatePgpChat(message, true);
+ }
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ Presences presences = message.getConversation().getContact()
+ .getPresences();
+ if ((message.getPresence() != null)
+ && (presences.has(message.getPresence()))) {
+ markMessage(message, Message.STATUS_OFFERED);
+ mJingleConnectionManager.createNewConnection(message);
+ } else {
+ if (presences.size() == 1) {
+ String presence = presences.asStringArray()[0];
+ message.setPresence(presence);
+ markMessage(message, Message.STATUS_OFFERED);
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ }
+ }
+ if (packet != null) {
+ if (!account.getXmppConnection().getFeatures().sm()
+ && message.getConversation().getMode() != Conversation.MODE_MULTI) {
+ markMessage(message, Message.STATUS_SEND);
+ } else {
+ markMessage(message, Message.STATUS_UNSEND);
+ }
+ sendMessagePacket(account, packet);
+ }
+ }
+
+ public void fetchRosterFromServer(Account account) {
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+ if (!"".equals(account.getRosterVersion())) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": fetching roster version " + account.getRosterVersion());
+ } else {
+ Log.d(Config.LOGTAG, account.getJid() + ": fetching roster");
+ }
+ iqPacket.query("jabber:iq:roster").setAttribute("ver",
+ account.getRosterVersion());
+ account.getXmppConnection().sendIqPacket(iqPacket,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account,
+ IqPacket packet) {
+ Element query = packet.findChild("query");
+ if (query != null) {
+ account.getRoster().markAllAsNotInRoster();
+ mIqParser.rosterItems(account, query);
+ }
+ }
+ });
+ }
+
+ public void fetchBookmarks(Account account) {
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+ Element query = iqPacket.query("jabber:iq:private");
+ query.addChild("storage", "storage:bookmarks");
+ OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element query = packet.query();
+ List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
+ Element storage = query.findChild("storage",
+ "storage:bookmarks");
+ if (storage != null) {
+ for (Element item : storage.getChildren()) {
+ if (item.getName().equals("conference")) {
+ Bookmark bookmark = Bookmark.parse(item, account);
+ bookmarks.add(bookmark);
+ Conversation conversation = find(bookmark);
+ if (conversation != null) {
+ conversation.setBookmark(bookmark);
+ } else {
+ if (bookmark.autojoin()) {
+ conversation = findOrCreateConversation(
+ account, bookmark.getJid(), true);
+ conversation.setBookmark(bookmark);
+ joinMuc(conversation);
+ }
+ }
+ }
+ }
+ }
+ account.setBookmarks(bookmarks);
+ }
+ };
+ sendIqPacket(account, iqPacket, callback);
+
+ }
+
+ public void pushBookmarks(Account account) {
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET);
+ Element query = iqPacket.query("jabber:iq:private");
+ Element storage = query.addChild("storage", "storage:bookmarks");
+ for (Bookmark bookmark : account.getBookmarks()) {
+ storage.addChild(bookmark);
+ }
+ sendIqPacket(account, iqPacket, null);
+ }
+
+ private void mergePhoneContactsWithRoster() {
+ PhoneHelper.loadPhoneContacts(getApplicationContext(),
+ new OnPhoneContactsLoadedListener() {
+ @Override
+ public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
+ for (Account account : accounts) {
+ account.getRoster().clearSystemAccounts();
+ }
+ for (Bundle phoneContact : phoneContacts) {
+ for (Account account : accounts) {
+ String jid = phoneContact.getString("jid");
+ Contact contact = account.getRoster()
+ .getContact(jid);
+ String systemAccount = phoneContact
+ .getInt("phoneid")
+ + "#"
+ + phoneContact.getString("lookup");
+ contact.setSystemAccount(systemAccount);
+ contact.setPhotoUri(phoneContact
+ .getString("photouri"));
+ contact.setSystemName(phoneContact
+ .getString("displayname"));
+ getAvatarService().clear(contact);
+ }
+ }
+ }
+ });
+ }
+
+ public List<Conversation> getConversations() {
+ if (this.conversations == null) {
+ Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
+ for (Account account : this.accounts) {
+ accountLookupTable.put(account.getUuid(), account);
+ }
+ this.conversations = databaseBackend
+ .getConversations(Conversation.STATUS_AVAILABLE);
+ for (Conversation conv : this.conversations) {
+ Account account = accountLookupTable.get(conv.getAccountUuid());
+ conv.setAccount(account);
+ conv.setMessages(databaseBackend.getMessages(conv, 50));
+ checkDeletedFiles(conv);
+ }
+ }
+ return this.conversations;
+ }
+
+ private void checkDeletedFiles(Conversation conversation) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getType() == Message.TYPE_IMAGE
+ && message.getEncryption() != Message.ENCRYPTION_PGP) {
+ if (!getFileBackend().isFileAvailable(message)) {
+ message.setDownloadable(new DeletedDownloadable());
+ }
+ }
+ }
+ }
+
+ private void markFileDeleted(String uuid) {
+ for (Conversation conversation : getConversations()) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getType() == Message.TYPE_IMAGE
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && message.getUuid().equals(uuid)) {
+ if (!getFileBackend().isFileAvailable(message)) {
+ message.setDownloadable(new DeletedDownloadable());
+ updateConversationUi();
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ public void populateWithOrderedConversations(List<Conversation> list) {
+ populateWithOrderedConversations(list, true);
+ }
+
+ public void populateWithOrderedConversations(List<Conversation> 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<Conversation>() {
+ @Override
+ public int compare(Conversation lhs, Conversation rhs) {
+ Message left = lhs.getLatestMessage();
+ Message right = rhs.getLatestMessage();
+ if (left.getTimeSent() > right.getTimeSent()) {
+ return -1;
+ } else if (left.getTimeSent() < right.getTimeSent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ }
+
+ public int loadMoreMessages(Conversation conversation, long timestamp) {
+ List<Message> messages = databaseBackend.getMessages(conversation, 50,
+ timestamp);
+ for (Message message : messages) {
+ message.setConversation(conversation);
+ }
+ conversation.addAll(0, messages);
+ return messages.size();
+ }
+
+ public List<Account> getAccounts() {
+ return this.accounts;
+ }
+
+ public Conversation find(List<Conversation> haystack, Contact contact) {
+ for (Conversation conversation : haystack) {
+ if (conversation.getContact() == contact) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation find(List<Conversation> haystack, Account account,
+ String jid) {
+ for (Conversation conversation : haystack) {
+ if ((account == null || conversation.getAccount().equals(account))
+ && (conversation.getContactJid().split("/", 2)[0]
+ .equals(jid))) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findOrCreateConversation(Account account, String jid,
+ boolean muc) {
+ Conversation conversation = find(account, jid);
+ if (conversation != null) {
+ return conversation;
+ }
+ conversation = databaseBackend.findConversation(account, jid);
+ if (conversation != null) {
+ conversation.setStatus(Conversation.STATUS_AVAILABLE);
+ conversation.setAccount(account);
+ if (muc) {
+ conversation.setMode(Conversation.MODE_MULTI);
+ } else {
+ conversation.setMode(Conversation.MODE_SINGLE);
+ }
+ conversation.setMessages(databaseBackend.getMessages(conversation,
+ 50));
+ this.databaseBackend.updateConversation(conversation);
+ } else {
+ String conversationName;
+ Contact contact = account.getRoster().getContact(jid);
+ if (contact != null) {
+ conversationName = contact.getDisplayName();
+ } else {
+ conversationName = jid.split("@")[0];
+ }
+ if (muc) {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_MULTI);
+ } else {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_SINGLE);
+ }
+ this.databaseBackend.createConversation(conversation);
+ }
+ this.conversations.add(conversation);
+ updateConversationUi();
+ return conversation;
+ }
+
+ public void archiveConversation(Conversation conversation) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null && bookmark.autojoin()) {
+ bookmark.setAutojoin(false);
+ pushBookmarks(bookmark.getAccount());
+ }
+ }
+ leaveMuc(conversation);
+ } else {
+ conversation.endOtrIfNeeded();
+ }
+ this.databaseBackend.updateConversation(conversation);
+ this.conversations.remove(conversation);
+ updateConversationUi();
+ }
+
+ public void clearConversationHistory(Conversation conversation) {
+ this.databaseBackend.deleteMessagesInConversation(conversation);
+ this.fileBackend.removeFiles(conversation);
+ conversation.getMessages().clear();
+ updateConversationUi();
+ }
+
+ public int getConversationCount() {
+ return this.databaseBackend.getConversationCount();
+ }
+
+ public void createAccount(Account account) {
+ databaseBackend.createAccount(account);
+ this.accounts.add(account);
+ this.reconnectAccount(account, false);
+ updateAccountUi();
+ }
+
+ public void updateAccount(Account account) {
+ this.statusListener.onStatusChanged(account);
+ databaseBackend.updateAccount(account);
+ reconnectAccount(account, false);
+ updateAccountUi();
+ UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
+ }
+
+ public void deleteAccount(Account account) {
+ for (Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ conversation.endOtrIfNeeded();
+ }
+ conversations.remove(conversation);
+ }
+ }
+ if (account.getXmppConnection() != null) {
+ this.disconnect(account, true);
+ }
+ databaseBackend.deleteAccount(account);
+ this.accounts.remove(account);
+ updateAccountUi();
+ UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
+ }
+
+ public void setOnConversationListChangedListener(
+ OnConversationUpdate listener) {
+ if (!isScreenOn()) {
+ Log.d(Config.LOGTAG,
+ "ignoring setOnConversationListChangedListener");
+ return;
+ }
+ synchronized (this.convChangedListenerCount) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnConversationUpdate = listener;
+ this.mNotificationService.setIsInForeground(true);
+ this.convChangedListenerCount++;
+ }
+ }
+
+ public void removeOnConversationListChangedListener() {
+ synchronized (this.convChangedListenerCount) {
+ this.convChangedListenerCount--;
+ if (this.convChangedListenerCount <= 0) {
+ this.convChangedListenerCount = 0;
+ this.mOnConversationUpdate = null;
+ this.mNotificationService.setIsInForeground(false);
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnAccountListChangedListener(OnAccountUpdate listener) {
+ if (!isScreenOn()) {
+ Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
+ return;
+ }
+ synchronized (this.accountChangedListenerCount) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnAccountUpdate = listener;
+ this.accountChangedListenerCount++;
+ }
+ }
+
+ public void removeOnAccountListChangedListener() {
+ synchronized (this.accountChangedListenerCount) {
+ this.accountChangedListenerCount--;
+ if (this.accountChangedListenerCount <= 0) {
+ this.mOnAccountUpdate = null;
+ this.accountChangedListenerCount = 0;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnRosterUpdateListener(OnRosterUpdate listener) {
+ if (!isScreenOn()) {
+ Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
+ return;
+ }
+ synchronized (this.rosterChangedListenerCount) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnRosterUpdate = listener;
+ this.rosterChangedListenerCount++;
+ }
+ }
+
+ public void removeOnRosterUpdateListener() {
+ synchronized (this.rosterChangedListenerCount) {
+ this.rosterChangedListenerCount--;
+ if (this.rosterChangedListenerCount <= 0) {
+ this.rosterChangedListenerCount = 0;
+ this.mOnRosterUpdate = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ private boolean checkListeners() {
+ return (this.mOnAccountUpdate == null
+ && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null);
+ }
+
+ private void switchToForeground() {
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null && connection.getFeatures().csi()) {
+ connection.sendActive();
+ }
+ }
+ }
+ Log.d(Config.LOGTAG, "app switched into foreground");
+ }
+
+ private void switchToBackground() {
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null && connection.getFeatures().csi()) {
+ connection.sendInactive();
+ }
+ }
+ }
+ this.mNotificationService.setIsInForeground(false);
+ Log.d(Config.LOGTAG, "app switched into background");
+ }
+
+ private boolean isScreenOn() {
+ PowerManager pm = (PowerManager) this
+ .getSystemService(Context.POWER_SERVICE);
+ return pm.isScreenOn();
+ }
+
+ public void connectMultiModeConversations(Account account) {
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); i++) {
+ Conversation conversation = conversations.get(i);
+ if ((conversation.getMode() == Conversation.MODE_MULTI)
+ && (conversation.getAccount() == account)) {
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void joinMuc(Conversation conversation) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ Log.d(Config.LOGTAG,
+ "joining conversation " + conversation.getContactJid());
+ String nick = conversation.getMucOptions().getProposedNick();
+ conversation.getMucOptions().setJoinNick(nick);
+ PresencePacket packet = new PresencePacket();
+ String joinJid = conversation.getMucOptions().getJoinJid();
+ packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
+ if (conversation.getMucOptions().getPassword() != null) {
+ Element password = x.addChild("password");
+ password.setContent(conversation.getMucOptions().getPassword());
+ }
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ if (conversation.getMessages().size() != 0) {
+ final SimpleDateFormat mDateFormat = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ Date date = new Date(conversation.getLatestMessage()
+ .getTimeSent() + 1000);
+ x.addChild("history").setAttribute("since",
+ mDateFormat.format(date));
+ }
+ packet.addChild(x);
+ sendPresencePacket(account, packet);
+ if (!joinJid.equals(conversation.getContactJid())) {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ }
+ } else {
+ account.pendingConferenceJoins.add(conversation);
+ }
+ }
+
+ private OnRenameListener renameListener = null;
+ private IqGenerator mIqGenerator = new IqGenerator(this);
+
+ public void setOnRenameListener(OnRenameListener listener) {
+ this.renameListener = listener;
+ }
+
+ public void providePasswordForMuc(Conversation conversation, String password) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.getMucOptions().setPassword(password);
+ if (conversation.getBookmark() != null) {
+ conversation.getBookmark().setAutojoin(true);
+ pushBookmarks(conversation.getAccount());
+ }
+ databaseBackend.updateConversation(conversation);
+ joinMuc(conversation);
+ }
+ }
+
+ public void renameInMuc(final Conversation conversation, final String nick) {
+ final MucOptions options = conversation.getMucOptions();
+ options.setJoinNick(nick);
+ if (options.online()) {
+ Account account = conversation.getAccount();
+ options.setOnRenameListener(new OnRenameListener() {
+
+ @Override
+ public void onRename(boolean success) {
+ if (renameListener != null) {
+ renameListener.onRename(success);
+ }
+ if (success) {
+ conversation.setContactJid(conversation.getMucOptions()
+ .getJoinJid());
+ databaseBackend.updateConversation(conversation);
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ }
+ }
+ });
+ options.flagAboutToRename();
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("to", options.getJoinJid());
+ packet.setAttribute("from", conversation.getAccount().getFullJid());
+
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ sendPresencePacket(account, packet);
+ } else {
+ conversation.setContactJid(options.getJoinJid());
+ databaseBackend.updateConversation(conversation);
+ if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void leaveMuc(Conversation conversation) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
+ packet.setAttribute("from", conversation.getAccount().getFullJid());
+ packet.setAttribute("type", "unavailable");
+ sendPresencePacket(conversation.getAccount(), packet);
+ conversation.getMucOptions().setOffline();
+ conversation.deregisterWithBookmark();
+ Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+ + ": leaving muc " + conversation.getContactJid());
+ } else {
+ account.pendingConferenceLeaves.add(conversation);
+ }
+ }
+
+ public void disconnect(Account account, boolean force) {
+ if ((account.getStatus() == Account.STATUS_ONLINE)
+ || (account.getStatus() == Account.STATUS_DISABLED)) {
+ if (!force) {
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); i++) {
+ Conversation conversation = conversations.get(i);
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else {
+ if (conversation.endOtrIfNeeded()) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": ended otr session with "
+ + conversation.getContactJid());
+ }
+ }
+ }
+ }
+ }
+ account.getXmppConnection().disconnect(force);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public void updateMessage(Message message) {
+ databaseBackend.updateMessage(message);
+ updateConversationUi();
+ }
+
+ protected void syncDirtyContacts(Account account) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ pushContactToServer(contact);
+ }
+ if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
+ deleteContactOnServer(contact);
+ }
+ }
+ }
+
+ public void createContact(Contact contact) {
+ SharedPreferences sharedPref = getPreferences();
+ boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
+ if (autoGrant) {
+ contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.setOption(Contact.Options.ASKING);
+ }
+ pushContactToServer(contact);
+ }
+
+ public void onOtrSessionEstablished(Conversation conversation) {
+ Account account = conversation.getAccount();
+ List<Message> messages = conversation.getMessages();
+ Session otrSession = conversation.getOtrSession();
+ Log.d(Config.LOGTAG,
+ account.getJid() + " otr session established with "
+ + conversation.getContactJid() + "/"
+ + otrSession.getSessionID().getUserID());
+ for (int i = 0; i < messages.size(); ++i) {
+ Message msg = messages.get(i);
+ if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
+ && (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
+ msg.setPresence(otrSession.getSessionID().getUserID());
+ if (msg.getType() == Message.TYPE_TEXT) {
+ MessagePacket outPacket = mMessageGenerator
+ .generateOtrChat(msg, true);
+ if (outPacket != null) {
+ msg.setStatus(Message.STATUS_SEND);
+ databaseBackend.updateMessage(msg);
+ sendMessagePacket(account, outPacket);
+ }
+ } else if (msg.getType() == Message.TYPE_IMAGE) {
+ mJingleConnectionManager.createNewConnection(msg);
+ }
+ }
+ }
+ updateConversationUi();
+ }
+
+ public boolean renewSymmetricKey(Conversation conversation) {
+ Account account = conversation.getAccount();
+ byte[] symmetricKey = new byte[32];
+ this.mRandom.nextBytes(symmetricKey);
+ Session otrSession = conversation.getOtrSession();
+ if (otrSession != null) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.setFrom(account.getFullJid());
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ packet.setTo(otrSession.getSessionID().getAccountID() + "/"
+ + otrSession.getSessionID().getUserID());
+ try {
+ packet.setBody(otrSession
+ .transformSending(CryptoHelper.FILETRANSFER
+ + CryptoHelper.bytesToHex(symmetricKey)));
+ sendMessagePacket(account, packet);
+ conversation.setSymmetricKey(symmetricKey);
+ return true;
+ } catch (OtrException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public void pushContactToServer(Contact contact) {
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.setOption(Contact.Options.DIRTY_PUSH);
+ Account account = contact.getAccount();
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ boolean ask = contact.getOption(Contact.Options.ASKING);
+ boolean sendUpdates = contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
+ && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.query("jabber:iq:roster").addChild(contact.asElement());
+ account.getXmppConnection().sendIqPacket(iq, null);
+ if (sendUpdates) {
+ sendPresencePacket(account,
+ mPresenceGenerator.sendPresenceUpdatesTo(contact));
+ }
+ if (ask) {
+ sendPresencePacket(account,
+ mPresenceGenerator.requestPresenceUpdatesFrom(contact));
+ }
+ }
+ }
+
+ public void publishAvatar(Account account, Uri image,
+ final UiCallback<Avatar> callback) {
+ final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
+ final int size = Config.AVATAR_SIZE;
+ final Avatar avatar = getFileBackend()
+ .getPepAvatar(image, size, format);
+ if (avatar != null) {
+ avatar.height = size;
+ avatar.width = size;
+ if (format.equals(Bitmap.CompressFormat.WEBP)) {
+ avatar.type = "image/webp";
+ } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
+ avatar.type = "image/jpeg";
+ } else if (format.equals(Bitmap.CompressFormat.PNG)) {
+ avatar.type = "image/png";
+ }
+ if (!getFileBackend().save(avatar)) {
+ callback.error(R.string.error_saving_avatar, avatar);
+ return;
+ }
+ IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
+ this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ if (result.getType() == IqPacket.TYPE_RESULT) {
+ IqPacket packet = XmppConnectionService.this.mIqGenerator
+ .publishAvatarMetadata(avatar);
+ sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket result) {
+ if (result.getType() == IqPacket.TYPE_RESULT) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ callback.success(avatar);
+ } else {
+ callback.error(
+ R.string.error_publish_avatar_server_reject,
+ avatar);
+ }
+ }
+ });
+ } else {
+ callback.error(
+ R.string.error_publish_avatar_server_reject,
+ avatar);
+ }
+ }
+ });
+ } else {
+ callback.error(R.string.error_publish_avatar_converting, null);
+ }
+ }
+
+ public void fetchAvatar(Account account, Avatar avatar) {
+ fetchAvatar(account, avatar, null);
+ }
+
+ public void fetchAvatar(Account account, final Avatar avatar,
+ final UiCallback<Avatar> callback) {
+ IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
+ sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ final String ERROR = account.getJid()
+ + ": fetching avatar for " + avatar.owner + " failed ";
+ if (result.getType() == IqPacket.TYPE_RESULT) {
+ avatar.image = mIqParser.avatarData(result);
+ if (avatar.image != null) {
+ if (getFileBackend().save(avatar)) {
+ if (account.getJid().equals(avatar.owner)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ getAvatarService().clear(account);
+ updateConversationUi();
+ updateAccountUi();
+ } else {
+ Contact contact = account.getRoster()
+ .getContact(avatar.owner);
+ contact.setAvatar(avatar.getFilename());
+ getAvatarService().clear(contact);
+ updateConversationUi();
+ updateRosterUi();
+ }
+ if (callback != null) {
+ callback.success(avatar);
+ }
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": succesfully fetched avatar for "
+ + avatar.owner);
+ return;
+ }
+ } else {
+
+ Log.d(Config.LOGTAG, ERROR + "(parsing error)");
+ }
+ } else {
+ Element error = result.findChild("error");
+ if (error == null) {
+ Log.d(Config.LOGTAG, ERROR + "(server error)");
+ } else {
+ Log.d(Config.LOGTAG, ERROR + error.toString());
+ }
+ }
+ if (callback != null) {
+ callback.error(0, null);
+ }
+
+ }
+ });
+ }
+
+ public void checkForAvatar(Account account,
+ final UiCallback<Avatar> callback) {
+ IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
+ this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_RESULT) {
+ Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub != null) {
+ Element items = pubsub.findChild("items");
+ if (items != null) {
+ Avatar avatar = Avatar.parseMetadata(items);
+ if (avatar != null) {
+ avatar.owner = account.getJid();
+ if (fileBackend.isAvatarCached(avatar)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ getAvatarService().clear(account);
+ callback.success(avatar);
+ } else {
+ fetchAvatar(account, avatar, callback);
+ }
+ return;
+ }
+ }
+ }
+ }
+ callback.error(0, null);
+ }
+ });
+ }
+
+ public void deleteContactOnServer(Contact contact) {
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ contact.setOption(Contact.Options.DIRTY_DELETE);
+ Account account = contact.getAccount();
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ Element item = iq.query("jabber:iq:roster").addChild("item");
+ item.setAttribute("jid", contact.getJid());
+ item.setAttribute("subscription", "remove");
+ account.getXmppConnection().sendIqPacket(iq, null);
+ }
+ }
+
+ public void updateConversation(Conversation conversation) {
+ this.databaseBackend.updateConversation(conversation);
+ }
+
+ public void reconnectAccount(final Account account, final boolean force) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (account.getXmppConnection() != null) {
+ disconnect(account, force);
+ }
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (account.getXmppConnection() == null) {
+ account.setXmppConnection(createConnection(account));
+ }
+ Thread thread = new Thread(account.getXmppConnection());
+ thread.start();
+ scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
+ false);
+ } else {
+ account.getRoster().clearPresences();
+ account.setXmppConnection(null);
+ }
+ }
+ }).start();
+ }
+
+ public void invite(Conversation conversation, String contact) {
+ MessagePacket packet = mMessageGenerator.invite(conversation, contact);
+ sendMessagePacket(conversation.getAccount(), packet);
+ }
+
+ public void resetSendingToWaiting(Account account) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getType() != Message.TYPE_IMAGE
+ && message.getStatus() == Message.STATUS_UNSEND) {
+ markMessage(message, Message.STATUS_WAITING);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean markMessage(Account account, String recipient, String uuid,
+ int status) {
+ if (uuid == null) {
+ return false;
+ } else {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getContactJid().equals(recipient)
+ && conversation.getAccount().equals(account)) {
+ return markMessage(conversation, uuid, status);
+ }
+ }
+ return false;
+ }
+ }
+
+ public boolean markMessage(Conversation conversation, String uuid,
+ int status) {
+ if (uuid == null) {
+ return false;
+ } else {
+ for (Message message : conversation.getMessages()) {
+ if (uuid.equals(message.getUuid())
+ || (message.getStatus() >= Message.STATUS_SEND && uuid
+ .equals(message.getRemoteMsgId()))) {
+ markMessage(message, status);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public void markMessage(Message message, int status) {
+ if (status == Message.STATUS_SEND_FAILED
+ && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
+ .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
+ return;
+ }
+ message.setStatus(status);
+ databaseBackend.updateMessage(message);
+ updateConversationUi();
+ }
+
+ public SharedPreferences getPreferences() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+ }
+
+ public boolean forceEncryption() {
+ return getPreferences().getBoolean("force_encryption", false);
+ }
+
+ public boolean confirmMessages() {
+ return getPreferences().getBoolean("confirm_messages", true);
+ }
+
+ public boolean saveEncryptedMessages() {
+ return !getPreferences().getBoolean("dont_save_encrypted", false);
+ }
+
+ public boolean indicateReceived() {
+ return getPreferences().getBoolean("indicate_received", false);
+ }
+
+ public void updateConversationUi() {
+ if (mOnConversationUpdate != null) {
+ mOnConversationUpdate.onConversationUpdate();
+ }
+ }
+
+ public void updateAccountUi() {
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ }
+
+ public void updateRosterUi() {
+ if (mOnRosterUpdate != null) {
+ mOnRosterUpdate.onRosterUpdate();
+ }
+ }
+
+ public Account findAccountByJid(String accountJid) {
+ for (Account account : this.accounts) {
+ if (account.getJid().equals(accountJid)) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findConversationByUuid(String uuid) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getUuid().equals(uuid)) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public void markRead(Conversation conversation, boolean calledByUi) {
+ mNotificationService.clear(conversation);
+ String id = conversation.getLatestMarkableMessageId();
+ conversation.markRead();
+ if (confirmMessages() && id != null && calledByUi) {
+ Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+ + ": sending read marker for " + conversation.getName());
+ Account account = conversation.getAccount();
+ String to = conversation.getContactJid();
+ this.sendMessagePacket(conversation.getAccount(),
+ mMessageGenerator.confirm(account, to, id));
+ }
+ if (!calledByUi) {
+ updateConversationUi();
+ }
+ }
+
+ public void failWaitingOtrMessages(Conversation conversation) {
+ for (Message message : conversation.getMessages()) {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR
+ && message.getStatus() == Message.STATUS_WAITING) {
+ markMessage(message, Message.STATUS_SEND_FAILED);
+ }
+ }
+ }
+
+ public SecureRandom getRNG() {
+ return this.mRandom;
+ }
+
+ public MemorizingTrustManager getMemorizingTrustManager() {
+ return this.mMemorizingTrustManager;
+ }
+
+ public PowerManager getPowerManager() {
+ return this.pm;
+ }
+
+ public LruCache<String, Bitmap> getBitmapCache() {
+ return this.mBitmapCache;
+ }
+
+ public void replyWithNotAcceptable(Account account, MessagePacket packet) {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ MessagePacket error = this.mMessageGenerator
+ .generateNotAcceptable(packet);
+ sendMessagePacket(account, error);
+ }
+ }
+
+ public void syncRosterToDisk(final Account account) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ databaseBackend.writeRoster(account.getRoster());
+ }
+ }).start();
+
+ }
+
+ public List<String> getKnownHosts() {
+ List<String> hosts = new ArrayList<String>();
+ for (Account account : getAccounts()) {
+ if (!hosts.contains(account.getServer())) {
+ hosts.add(account.getServer());
+ }
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster()) {
+ String server = contact.getServer();
+ if (server != null && !hosts.contains(server)) {
+ hosts.add(server);
+ }
+ }
+ }
+ }
+ return hosts;
+ }
+
+ public List<String> getKnownConferenceHosts() {
+ ArrayList<String> mucServers = new ArrayList<String>();
+ for (Account account : accounts) {
+ if (account.getXmppConnection() != null) {
+ String server = account.getXmppConnection().getMucServer();
+ if (server != null && !mucServers.contains(server)) {
+ mucServers.add(server);
+ }
+ }
+ }
+ return mucServers;
+ }
+
+ public void sendMessagePacket(Account account, MessagePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendMessagePacket(packet);
+ }
+ }
+
+ public void sendPresencePacket(Account account, PresencePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendPresencePacket(packet);
+ }
+ }
+
+ public void sendIqPacket(Account account, IqPacket packet,
+ OnIqPacketReceived callback) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendIqPacket(packet, callback);
+ }
+ }
+
+ public MessageGenerator getMessageGenerator() {
+ return this.mMessageGenerator;
+ }
+
+ public PresenceGenerator getPresenceGenerator() {
+ return this.mPresenceGenerator;
+ }
+
+ public IqGenerator getIqGenerator() {
+ return this.mIqGenerator;
+ }
+
+ public JingleConnectionManager getJingleConnectionManager() {
+ return this.mJingleConnectionManager;
+ }
+
+ public interface OnConversationUpdate {
+ public void onConversationUpdate();
+ }
+
+ public interface OnAccountUpdate {
+ public void onAccountUpdate();
+ }
+
+ public interface OnRosterUpdate {
+ public void onRosterUpdate();
+ }
+
+ public List<Contact> findContacts(String jid) {
+ ArrayList<Contact> contacts = new ArrayList<Contact>();
+ for (Account account : getAccounts()) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ Contact contact = account.getRoster().getContactFromRoster(jid);
+ if (contact != null) {
+ contacts.add(contact);
+ }
+ }
+ }
+ return contacts;
+ }
+
+ public NotificationService getNotificationService() {
+ return this.mNotificationService;
+ }
+
+ public HttpConnectionManager getHttpConnectionManager() {
+ return this.mHttpConnectionManager;
+ }
+
+ private class DeletedDownloadable implements Downloadable {
+
+ @Override
+ public boolean start() {
+ return false;
+ }
+
+ @Override
+ public int getStatus() {
+ return Downloadable.STATUS_DELETED;
+ }
+
+ @Override
+ public long getFileSize() {
+ return 0;
+ }
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
new file mode 100644
index 00000000..62a2cbe1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
@@ -0,0 +1,145 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.adapter.ListItemAdapter;
+
+public class ChooseContactActivity extends XmppActivity {
+
+ private ListView mListView;
+ private ArrayList<ListItem> contacts = new ArrayList<ListItem>();
+ private ArrayAdapter<ListItem> mContactsAdapter;
+
+ private EditText mSearchEditText;
+
+ private TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ filterContacts(editable.toString());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ }
+ };
+
+ private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ mSearchEditText.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mSearchEditText.requestFocus();
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mSearchEditText,
+ InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ mSearchEditText.setText("");
+ filterContacts(null);
+ return true;
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_choose_contact);
+ mListView = (ListView) findViewById(R.id.choose_contact_list);
+ mListView.setFastScrollEnabled(true);
+ mContactsAdapter = new ListItemAdapter(this, contacts);
+ mListView.setAdapter(mContactsAdapter);
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1,
+ int position, long arg3) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ Intent request = getIntent();
+ Intent data = new Intent();
+ ListItem mListItem = contacts.get(position);
+ data.putExtra("contact", mListItem.getJid());
+ String account = request.getStringExtra("account");
+ if (account == null && mListItem instanceof Contact) {
+ account = ((Contact) mListItem).getAccount().getJid();
+ }
+ data.putExtra("account", account);
+ data.putExtra("conversation",
+ request.getStringExtra("conversation"));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.choose_contact, menu);
+ MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search);
+ View mSearchView = menuSearchView.getActionView();
+ mSearchEditText = (EditText) mSearchView
+ .findViewById(R.id.search_field);
+ mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+ menuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+ return true;
+ }
+
+ @Override
+ void onBackendConnected() {
+ filterContacts(null);
+ }
+
+ protected void filterContacts(String needle) {
+ this.contacts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.STATUS_DISABLED) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster() && contact.match(needle)) {
+ this.contacts.add(contact);
+ }
+ }
+ }
+ }
+ Collections.sort(this.contacts);
+ mContactsAdapter.notifyDataSetChanged();
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
new file mode 100644
index 00000000..6b4642cb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -0,0 +1,280 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.MucOptions.User;
+import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.IntentSender.SendIntentException;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class ConferenceDetailsActivity extends XmppActivity {
+ public static final String ACTION_VIEW_MUC = "view_muc";
+ private Conversation conversation;
+ private TextView mYourNick;
+ private ImageView mYourPhoto;
+ private ImageButton mEditNickButton;
+ private TextView mRoleAffiliaton;
+ private TextView mFullJid;
+ private TextView mAccountJid;
+ private LinearLayout membersView;
+ private LinearLayout mMoreDetails;
+ private Button mInviteButton;
+ private String uuid = null;
+
+ private OnClickListener inviteListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ inviteToConversation(conversation);
+ }
+ };
+
+ private List<User> users = new ArrayList<MucOptions.User>();
+ private OnConversationUpdate onConvChanged = new OnConversationUpdate() {
+
+ @Override
+ public void onConversationUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ }
+ });
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_muc_details);
+ mYourNick = (TextView) findViewById(R.id.muc_your_nick);
+ mYourPhoto = (ImageView) findViewById(R.id.your_photo);
+ mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button);
+ mFullJid = (TextView) findViewById(R.id.muc_jabberid);
+ membersView = (LinearLayout) findViewById(R.id.muc_members);
+ mAccountJid = (TextView) findViewById(R.id.details_account);
+ mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details);
+ mMoreDetails.setVisibility(View.GONE);
+ mInviteButton = (Button) findViewById(R.id.invite);
+ mInviteButton.setOnClickListener(inviteListener);
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ mEditNickButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ quickEdit(conversation.getMucOptions().getActualNick(),
+ new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ xmppConnectionService.renameInMuc(conversation,
+ value);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ case R.id.action_edit_subject:
+ if (conversation != null) {
+ quickEdit(conversation.getName(), new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ MessagePacket packet = xmppConnectionService
+ .getMessageGenerator().conferenceSubject(
+ conversation, value);
+ xmppConnectionService.sendMessagePacket(
+ conversation.getAccount(), packet);
+ }
+ });
+ }
+ break;
+ }
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ public String getReadableRole(int role) {
+ switch (role) {
+ case User.ROLE_MODERATOR:
+ return getString(R.string.moderator);
+ case User.ROLE_PARTICIPANT:
+ return getString(R.string.participant);
+ case User.ROLE_VISITOR:
+ return getString(R.string.visitor);
+ default:
+ return "";
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.muc_details, menu);
+ return true;
+ }
+
+ @Override
+ void onBackendConnected() {
+ registerListener();
+ if (getIntent().getAction().equals(ACTION_VIEW_MUC)) {
+ this.uuid = getIntent().getExtras().getString("uuid");
+ }
+ if (uuid != null) {
+ this.conversation = xmppConnectionService
+ .findConversationByUuid(uuid);
+ if (this.conversation != null) {
+ populateView();
+ }
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnConversationListChangedListener();
+ }
+ super.onStop();
+ }
+
+ protected void registerListener() {
+ xmppConnectionService
+ .setOnConversationListChangedListener(this.onConvChanged);
+ xmppConnectionService.setOnRenameListener(new OnRenameListener() {
+
+ @Override
+ public void onRename(final boolean success) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ if (success) {
+ Toast.makeText(
+ ConferenceDetailsActivity.this,
+ getString(R.string.your_nick_has_been_changed),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(ConferenceDetailsActivity.this,
+ getString(R.string.nick_in_use),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ }
+ });
+ }
+
+ private void populateView() {
+ mAccountJid.setText(getString(R.string.using_account, conversation
+ .getAccount().getJid()));
+ mYourPhoto.setImageBitmap(avatarService().get(
+ conversation.getAccount(), getPixel(48)));
+ setTitle(conversation.getName());
+ mFullJid.setText(conversation.getContactJid().split("/", 2)[0]);
+ mYourNick.setText(conversation.getMucOptions().getActualNick());
+ mRoleAffiliaton = (TextView) findViewById(R.id.muc_role);
+ if (conversation.getMucOptions().online()) {
+ mMoreDetails.setVisibility(View.VISIBLE);
+ User self = conversation.getMucOptions().getSelf();
+ switch (self.getAffiliation()) {
+ case User.AFFILIATION_ADMIN:
+ mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " ("
+ + getString(R.string.admin) + ")");
+ break;
+ case User.AFFILIATION_OWNER:
+ mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " ("
+ + getString(R.string.owner) + ")");
+ break;
+ default:
+ mRoleAffiliaton.setText(getReadableRole(self.getRole()));
+ break;
+ }
+ }
+ this.users.clear();
+ this.users.addAll(conversation.getMucOptions().getUsers());
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ membersView.removeAllViews();
+ for (final User user : conversation.getMucOptions().getUsers()) {
+ View view = (View) inflater.inflate(R.layout.contact, membersView,
+ false);
+ TextView name = (TextView) view
+ .findViewById(R.id.contact_display_name);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView role = (TextView) view.findViewById(R.id.contact_jid);
+ if (user.getPgpKeyId() != 0) {
+ key.setVisibility(View.VISIBLE);
+ key.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ viewPgpKey(user);
+ }
+ });
+ key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
+ }
+ Bitmap bm;
+ Contact contact = user.getContact();
+ if (contact != null) {
+ bm = avatarService().get(contact, getPixel(48));
+ name.setText(contact.getDisplayName());
+ role.setText(user.getName() + " \u2022 "
+ + getReadableRole(user.getRole()));
+ } else {
+ bm = avatarService().get(user.getName(), getPixel(48));
+ name.setText(user.getName());
+ role.setText(getReadableRole(user.getRole()));
+ }
+ ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
+ iv.setImageBitmap(bm);
+ membersView.addView(view);
+ }
+ }
+
+ private void viewPgpKey(User user) {
+ PgpEngine pgp = xmppConnectionService.getPgpEngine();
+ if (pgp != null) {
+ PendingIntent intent = pgp.getIntentForKey(
+ conversation.getAccount(), user.getPgpKeyId());
+ if (intent != null) {
+ try {
+ startIntentSenderForResult(intent.getIntentSender(), 0,
+ null, 0, 0, 0);
+ } catch (SendIntentException e) {
+
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
new file mode 100644
index 00000000..ae26466e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -0,0 +1,436 @@
+package eu.siacs.conversations.ui;
+
+import java.util.Iterator;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.CheckBox;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.CompoundButton;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class ContactDetailsActivity extends XmppActivity {
+ public static final String ACTION_VIEW_CONTACT = "view_contact";
+
+ private Contact contact;
+
+ private String accountJid;
+ private String contactJid;
+
+ private TextView contactJidTv;
+ private TextView accountJidTv;
+ private TextView status;
+ private TextView lastseen;
+ private CheckBox send;
+ private CheckBox receive;
+ private QuickContactBadge badge;
+
+ private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ContactDetailsActivity.this.xmppConnectionService
+ .deleteContactOnServer(contact);
+ ContactDetailsActivity.this.finish();
+ }
+ };
+
+ private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ intent.setType(Contacts.CONTENT_ITEM_TYPE);
+ intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid());
+ intent.putExtra(Intents.Insert.IM_PROTOCOL,
+ CommonDataKinds.Im.PROTOCOL_JABBER);
+ intent.putExtra("finishActivityOnSaveCompleted", true);
+ ContactDetailsActivity.this.startActivityForResult(intent, 0);
+ }
+ };
+ private OnClickListener onBadgeClick = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ ContactDetailsActivity.this);
+ builder.setTitle(getString(R.string.action_add_phone_book));
+ builder.setMessage(getString(R.string.add_phone_book_text,
+ contact.getJid()));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.add), addToPhonebook);
+ builder.create().show();
+ }
+ };
+
+ private LinearLayout keys;
+
+ private OnRosterUpdate rosterUpdate = new OnRosterUpdate() {
+
+ @Override
+ public void onRosterUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ }
+ });
+ }
+ };
+
+ private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (isChecked) {
+ if (contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ xmppConnectionService.sendPresencePacket(contact
+ .getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .sendPresenceUpdatesTo(contact));
+ } else {
+ contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+ }
+ } else {
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ xmppConnectionService.sendPresencePacket(contact.getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .stopPresenceUpdatesTo(contact));
+ }
+ }
+ };
+
+ private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (isChecked) {
+ xmppConnectionService.sendPresencePacket(contact.getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .requestPresenceUpdatesFrom(contact));
+ } else {
+ xmppConnectionService.sendPresencePacket(contact.getAccount(),
+ xmppConnectionService.getPresenceGenerator()
+ .stopPresenceUpdatesFrom(contact));
+ }
+ }
+ };
+
+ private OnAccountUpdate accountUpdate = new OnAccountUpdate() {
+
+ @Override
+ public void onAccountUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ populateView();
+ }
+ });
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
+ this.accountJid = getIntent().getExtras().getString("account");
+ this.contactJid = getIntent().getExtras().getString("contact");
+ }
+ setContentView(R.layout.activity_contact_details);
+
+ contactJidTv = (TextView) findViewById(R.id.details_contactjid);
+ accountJidTv = (TextView) findViewById(R.id.details_account);
+ status = (TextView) findViewById(R.id.details_contactstatus);
+ lastseen = (TextView) findViewById(R.id.details_lastseen);
+ send = (CheckBox) findViewById(R.id.details_send_presence);
+ receive = (CheckBox) findViewById(R.id.details_receive_presence);
+ badge = (QuickContactBadge) findViewById(R.id.details_contact_badge);
+ keys = (LinearLayout) findViewById(R.id.details_contact_keys);
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ switch (menuItem.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ case R.id.action_delete_contact:
+ builder.setTitle(getString(R.string.action_delete_contact))
+ .setMessage(
+ getString(R.string.remove_contact_text,
+ contact.getJid()))
+ .setPositiveButton(getString(R.string.delete),
+ removeFromRoster).create().show();
+ break;
+ case R.id.action_edit_contact:
+ if (contact.getSystemAccount() == null) {
+ quickEdit(contact.getDisplayName(), new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ contact.setServerName(value);
+ ContactDetailsActivity.this.xmppConnectionService
+ .pushContactToServer(contact);
+ populateView();
+ }
+ });
+ } else {
+ Intent intent = new Intent(Intent.ACTION_EDIT);
+ String[] systemAccount = contact.getSystemAccount().split("#");
+ long id = Long.parseLong(systemAccount[0]);
+ Uri uri = Contacts.getLookupUri(id, systemAccount[1]);
+ intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE);
+ intent.putExtra("finishActivityOnSaveCompleted", true);
+ startActivity(intent);
+ }
+ break;
+ }
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.contact_details, menu);
+ return true;
+ }
+
+ private void populateView() {
+ send.setOnCheckedChangeListener(null);
+ receive.setOnCheckedChangeListener(null);
+ setTitle(contact.getDisplayName());
+ if (contact.getOption(Contact.Options.FROM)) {
+ send.setText(R.string.send_presence_updates);
+ send.setChecked(true);
+ } else if (contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ send.setChecked(false);
+ send.setText(R.string.send_presence_updates);
+ } else {
+ send.setText(R.string.preemptively_grant);
+ if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
+ send.setChecked(true);
+ } else {
+ send.setChecked(false);
+ }
+ }
+ if (contact.getOption(Contact.Options.TO)) {
+ receive.setText(R.string.receive_presence_updates);
+ receive.setChecked(true);
+ } else {
+ receive.setText(R.string.ask_for_presence_updates);
+ if (contact.getOption(Contact.Options.ASKING)) {
+ receive.setChecked(true);
+ } else {
+ receive.setChecked(false);
+ }
+ }
+ if (contact.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ receive.setEnabled(true);
+ send.setEnabled(true);
+ } else {
+ receive.setEnabled(false);
+ send.setEnabled(false);
+ }
+
+ send.setOnCheckedChangeListener(this.mOnSendCheckedChange);
+ receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
+
+ lastseen.setText(UIHelper.lastseen(getApplicationContext(),
+ contact.lastseen.time));
+
+ switch (contact.getMostAvailableStatus()) {
+ case Presences.CHAT:
+ status.setText(R.string.contact_status_free_to_chat);
+ status.setTextColor(mColorGreen);
+ break;
+ case Presences.ONLINE:
+ status.setText(R.string.contact_status_online);
+ status.setTextColor(mColorGreen);
+ break;
+ case Presences.AWAY:
+ status.setText(R.string.contact_status_away);
+ status.setTextColor(mColorOrange);
+ break;
+ case Presences.XA:
+ status.setText(R.string.contact_status_extended_away);
+ status.setTextColor(mColorOrange);
+ break;
+ case Presences.DND:
+ status.setText(R.string.contact_status_do_not_disturb);
+ status.setTextColor(mColorRed);
+ break;
+ case Presences.OFFLINE:
+ status.setText(R.string.contact_status_offline);
+ status.setTextColor(mSecondaryTextColor);
+ break;
+ default:
+ status.setText(R.string.contact_status_offline);
+ status.setTextColor(mSecondaryTextColor);
+ break;
+ }
+ if (contact.getPresences().size() > 1) {
+ contactJidTv.setText(contact.getJid() + " ("
+ + contact.getPresences().size() + ")");
+ } else {
+ contactJidTv.setText(contact.getJid());
+ }
+ accountJidTv.setText(getString(R.string.using_account, contact
+ .getAccount().getJid()));
+ prepareContactBadge(badge, contact);
+ if (contact.getSystemAccount() == null) {
+ badge.setOnClickListener(onBadgeClick);
+ }
+
+ keys.removeAllViews();
+ boolean hasKeys = false;
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ for (Iterator<String> iterator = contact.getOtrFingerprints()
+ .iterator(); iterator.hasNext();) {
+ hasKeys = true;
+ final String otrFingerprint = iterator.next();
+ View view = (View) inflater.inflate(R.layout.contact_key, keys,
+ false);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView keyType = (TextView) view.findViewById(R.id.key_type);
+ ImageButton remove = (ImageButton) view
+ .findViewById(R.id.button_remove);
+ remove.setVisibility(View.VISIBLE);
+ keyType.setText("OTR Fingerprint");
+ key.setText(otrFingerprint);
+ keys.addView(view);
+ remove.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ confirmToDeleteFingerprint(otrFingerprint);
+ }
+ });
+ }
+ if (contact.getPgpKeyId() != 0) {
+ hasKeys = true;
+ View view = (View) inflater.inflate(R.layout.contact_key, keys,
+ false);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView keyType = (TextView) view.findViewById(R.id.key_type);
+ keyType.setText("PGP Key ID");
+ key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
+ view.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService
+ .getPgpEngine();
+ if (pgp != null) {
+ PendingIntent intent = pgp.getIntentForKey(contact);
+ if (intent != null) {
+ try {
+ startIntentSenderForResult(
+ intent.getIntentSender(), 0, null, 0,
+ 0, 0);
+ } catch (SendIntentException e) {
+
+ }
+ }
+ }
+ }
+ });
+ keys.addView(view);
+ }
+ if (hasKeys) {
+ keys.setVisibility(View.VISIBLE);
+ } else {
+ keys.setVisibility(View.GONE);
+ }
+ }
+
+ private void prepareContactBadge(QuickContactBadge badge, Contact contact) {
+ if (contact.getSystemAccount() != null) {
+ String[] systemAccount = contact.getSystemAccount().split("#");
+ long id = Long.parseLong(systemAccount[0]);
+ badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1]));
+ }
+ badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
+ }
+
+ protected void confirmToDeleteFingerprint(final String fingerprint) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.delete_fingerprint);
+ builder.setMessage(R.string.sure_delete_fingerprint);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.delete,
+ new android.content.DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (contact.deleteOtrFingerprint(fingerprint)) {
+ populateView();
+ xmppConnectionService.syncRosterToDisk(contact
+ .getAccount());
+ }
+ }
+
+ });
+ builder.create().show();
+ }
+
+ @Override
+ public void onBackendConnected() {
+ xmppConnectionService.setOnRosterUpdateListener(this.rosterUpdate);
+ xmppConnectionService
+ .setOnAccountListChangedListener(this.accountUpdate);
+ if ((accountJid != null) && (contactJid != null)) {
+ Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ return;
+ }
+ this.contact = account.getRoster().getContact(contactJid);
+ populateView();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ xmppConnectionService.removeOnRosterUpdateListener();
+ xmppConnectionService.removeOnAccountListChangedListener();
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
new file mode 100644
index 00000000..91e1c81f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -0,0 +1,947 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.ConversationAdapter;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.app.PendingIntent;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.IntentSender.SendIntentException;
+import android.content.Intent;
+import android.support.v4.widget.SlidingPaneLayout;
+import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.Toast;
+
+public class ConversationActivity extends XmppActivity implements
+ OnAccountUpdate, OnConversationUpdate, OnRosterUpdate {
+
+ public static final String VIEW_CONVERSATION = "viewConversation";
+ public static final String CONVERSATION = "conversationUuid";
+ public static final String TEXT = "text";
+ public static final String PRESENCE = "eu.siacs.conversations.presence";
+
+ public static final int REQUEST_SEND_MESSAGE = 0x0201;
+ public static final int REQUEST_DECRYPT_PGP = 0x0202;
+ private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203;
+ private static final int REQUEST_IMAGE_CAPTURE = 0x0204;
+ private static final int REQUEST_RECORD_AUDIO = 0x0205;
+ private static final int REQUEST_SEND_PGP_IMAGE = 0x0206;
+ public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
+
+ private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
+ private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
+ private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303;
+ private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
+ private static final String STATE_PANEL_OPEN = "state_panel_open";
+
+ private String mOpenConverstaion = null;
+ private boolean mPanelOpen = true;
+
+ private View mContentView;
+
+ private List<Conversation> conversationList = new ArrayList<Conversation>();
+ private Conversation selectedConversation = null;
+ private ListView listView;
+
+ private boolean paneShouldBeOpen = true;
+ private ArrayAdapter<Conversation> listAdapter;
+
+ private Toast prepareImageToast;
+
+ private Uri pendingImageUri = null;
+
+ public List<Conversation> getConversationList() {
+ return this.conversationList;
+ }
+
+ public Conversation getSelectedConversation() {
+ return this.selectedConversation;
+ }
+
+ public void setSelectedConversation(Conversation conversation) {
+ this.selectedConversation = conversation;
+ }
+
+ public ListView getConversationListView() {
+ return this.listView;
+ }
+
+ public boolean shouldPaneBeOpen() {
+ return paneShouldBeOpen;
+ }
+
+ public void showConversationsOverview() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ mSlidingPaneLayout.openPane();
+ }
+ }
+
+ public void hideConversationsOverview() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ mSlidingPaneLayout.closePane();
+ }
+ }
+
+ public boolean isConversationsOverviewHideable() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ return mSlidingPaneLayout.isSlideable();
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isConversationsOverviewVisable() {
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ return mSlidingPaneLayout.isOpen();
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ mOpenConverstaion = savedInstanceState.getString(
+ STATE_OPEN_CONVERSATION, null);
+ mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true);
+ }
+
+ setContentView(R.layout.fragment_conversations_overview);
+
+ listView = (ListView) findViewById(R.id.list);
+
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setHomeButtonEnabled(false);
+
+ this.listAdapter = new ConversationAdapter(this, conversationList);
+ listView.setAdapter(this.listAdapter);
+
+ listView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View clickedView,
+ int position, long arg3) {
+ paneShouldBeOpen = false;
+ if (getSelectedConversation() != conversationList.get(position)) {
+ setSelectedConversation(conversationList.get(position));
+ swapConversationFragment();
+ } else {
+ hideConversationsOverview();
+ }
+ }
+ });
+ mContentView = findViewById(R.id.content_view_spl);
+ if (mContentView == null) {
+ mContentView = findViewById(R.id.content_view_ll);
+ }
+ if (mContentView instanceof SlidingPaneLayout) {
+ SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+ mSlidingPaneLayout.setParallaxDistance(150);
+ mSlidingPaneLayout
+ .setShadowResource(R.drawable.es_slidingpane_shadow);
+ mSlidingPaneLayout.setSliderFadeColor(0);
+ mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
+
+ @Override
+ public void onPanelOpened(View arg0) {
+ paneShouldBeOpen = true;
+ ActionBar ab = getActionBar();
+ if (ab != null) {
+ ab.setDisplayHomeAsUpEnabled(false);
+ ab.setHomeButtonEnabled(false);
+ ab.setTitle(R.string.app_name);
+ }
+ invalidateOptionsMenu();
+ hideKeyboard();
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.getNotificationService()
+ .setOpenConversation(null);
+ }
+ }
+
+ @Override
+ public void onPanelClosed(View arg0) {
+ paneShouldBeOpen = false;
+ if ((conversationList.size() > 0)
+ && (getSelectedConversation() != null)) {
+ openConversation(getSelectedConversation());
+ if (!getSelectedConversation().isRead()) {
+ xmppConnectionService.markRead(
+ getSelectedConversation(), true);
+ listView.invalidateViews();
+ }
+ }
+ }
+
+ @Override
+ public void onPanelSlide(View arg0, float arg1) {
+ // TODO Auto-generated method stub
+
+ }
+ });
+ }
+ }
+
+ public void openConversation(Conversation conversation) {
+ ActionBar ab = getActionBar();
+ if (ab != null) {
+ ab.setDisplayHomeAsUpEnabled(true);
+ ab.setHomeButtonEnabled(true);
+ if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE
+ || ConversationActivity.this
+ .useSubjectToIdentifyConference()) {
+ ab.setTitle(getSelectedConversation().getName());
+ } else {
+ ab.setTitle(getSelectedConversation().getContactJid()
+ .split("/")[0]);
+ }
+ }
+ invalidateOptionsMenu();
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.getNotificationService().setOpenConversation(
+ conversation);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.conversations, menu);
+ MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security);
+ MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive);
+ MenuItem menuMucDetails = (MenuItem) menu
+ .findItem(R.id.action_muc_details);
+ MenuItem menuContactDetails = (MenuItem) menu
+ .findItem(R.id.action_contact_details);
+ MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file);
+ MenuItem menuClearHistory = (MenuItem) menu
+ .findItem(R.id.action_clear_history);
+ MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add);
+ MenuItem menuInviteContact = (MenuItem) menu
+ .findItem(R.id.action_invite);
+ MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute);
+
+ if (isConversationsOverviewVisable()
+ && isConversationsOverviewHideable()) {
+ menuArchive.setVisible(false);
+ menuMucDetails.setVisible(false);
+ menuContactDetails.setVisible(false);
+ menuSecure.setVisible(false);
+ menuInviteContact.setVisible(false);
+ menuAttach.setVisible(false);
+ menuClearHistory.setVisible(false);
+ menuMute.setVisible(false);
+ } else {
+ menuAdd.setVisible(!isConversationsOverviewHideable());
+ if (this.getSelectedConversation() != null) {
+ if (this.getSelectedConversation().getLatestMessage()
+ .getEncryption() != Message.ENCRYPTION_NONE) {
+ menuSecure.setIcon(R.drawable.ic_action_secure);
+ }
+ if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) {
+ menuContactDetails.setVisible(false);
+ menuAttach.setVisible(false);
+ } else {
+ menuMucDetails.setVisible(false);
+ menuInviteContact.setVisible(false);
+ }
+ }
+ }
+ return true;
+ }
+
+ private void selectPresenceToAttachFile(final int attachmentChoice) {
+ selectPresence(getSelectedConversation(), new OnPresenceSelected() {
+
+ @Override
+ public void onPresenceSelected() {
+ if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+ pendingImageUri = xmppConnectionService.getFileBackend()
+ .getTakePhotoUri();
+ Intent takePictureIntent = new Intent(
+ MediaStore.ACTION_IMAGE_CAPTURE);
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
+ pendingImageUri);
+ if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
+ startActivityForResult(takePictureIntent,
+ REQUEST_IMAGE_CAPTURE);
+ }
+ } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+ Intent attachFileIntent = new Intent();
+ attachFileIntent.setType("image/*");
+ attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
+ Intent chooser = Intent.createChooser(attachFileIntent,
+ getString(R.string.attach_file));
+ startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG);
+ } else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
+ Intent intent = new Intent(
+ MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+ startActivityForResult(intent, REQUEST_RECORD_AUDIO);
+ }
+ }
+ });
+ }
+
+ private void attachFile(final int attachmentChoice) {
+ final Conversation conversation = getSelectedConversation();
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ if (hasPgp()) {
+ if (conversation.getContact().getPgpKeyId() != 0) {
+ xmppConnectionService.getPgpEngine().hasKey(
+ conversation.getContact(),
+ new UiCallback<Contact>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Contact contact) {
+ ConversationActivity.this.runIntent(pi,
+ attachmentChoice);
+ }
+
+ @Override
+ public void success(Contact contact) {
+ selectPresenceToAttachFile(attachmentChoice);
+ }
+
+ @Override
+ public void error(int error, Contact contact) {
+ displayErrorDialog(error);
+ }
+ });
+ } else {
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ fragment.showNoPGPKeyDialog(false,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_NONE);
+ xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ selectPresenceToAttachFile(attachmentChoice);
+ }
+ });
+ }
+ }
+ } else {
+ showInstallPgpDialog();
+ }
+ } else if (getSelectedConversation().getNextEncryption(
+ forceEncryption()) == Message.ENCRYPTION_NONE) {
+ selectPresenceToAttachFile(attachmentChoice);
+ } else {
+ selectPresenceToAttachFile(attachmentChoice);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ showConversationsOverview();
+ return true;
+ } else if (item.getItemId() == R.id.action_add) {
+ startActivity(new Intent(this, StartConversationActivity.class));
+ return true;
+ } else if (getSelectedConversation() != null) {
+ switch (item.getItemId()) {
+ case R.id.action_attach_file:
+ attachFileDialog();
+ break;
+ case R.id.action_archive:
+ this.endConversation(getSelectedConversation());
+ break;
+ case R.id.action_contact_details:
+ Contact contact = this.getSelectedConversation().getContact();
+ if (contact.showInRoster()) {
+ switchToContactDetails(contact);
+ } else {
+ showAddToRosterDialog(getSelectedConversation());
+ }
+ break;
+ case R.id.action_muc_details:
+ Intent intent = new Intent(this,
+ ConferenceDetailsActivity.class);
+ intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
+ intent.putExtra("uuid", getSelectedConversation().getUuid());
+ startActivity(intent);
+ break;
+ case R.id.action_invite:
+ inviteToConversation(getSelectedConversation());
+ break;
+ case R.id.action_security:
+ selectEncryptionDialog(getSelectedConversation());
+ break;
+ case R.id.action_clear_history:
+ clearHistoryDialog(getSelectedConversation());
+ break;
+ case R.id.action_mute:
+ muteConversationDialog(getSelectedConversation());
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void endConversation(Conversation conversation) {
+ conversation.setStatus(Conversation.STATUS_ARCHIVED);
+ paneShouldBeOpen = true;
+ showConversationsOverview();
+ xmppConnectionService.archiveConversation(conversation);
+ if (conversationList.size() > 0) {
+ setSelectedConversation(conversationList.get(0));
+ } else {
+ setSelectedConversation(null);
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ protected void clearHistoryDialog(final Conversation conversation) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.clear_conversation_history));
+ View dialogView = getLayoutInflater().inflate(
+ R.layout.dialog_clear_history, null);
+ final CheckBox endConversationCheckBox = (CheckBox) dialogView
+ .findViewById(R.id.end_conversation_checkbox);
+ builder.setView(dialogView);
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.delete_messages),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ConversationActivity.this.xmppConnectionService
+ .clearConversationHistory(conversation);
+ if (endConversationCheckBox.isChecked()) {
+ endConversation(conversation);
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ protected void attachFileDialog() {
+ View menuAttachFile = findViewById(R.id.action_attach_file);
+ if (menuAttachFile == null) {
+ return;
+ }
+ PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile);
+ attachFilePopup.inflate(R.menu.attachment_choices);
+ attachFilePopup
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.attach_choose_picture:
+ attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+ break;
+ case R.id.attach_take_picture:
+ attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+ break;
+ case R.id.attach_record_voice:
+ attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
+ break;
+ }
+ return false;
+ }
+ });
+ attachFilePopup.show();
+ }
+
+ protected void selectEncryptionDialog(final Conversation conversation) {
+ View menuItemView = findViewById(R.id.action_security);
+ if (menuItemView == null) {
+ return;
+ }
+ PopupMenu popup = new PopupMenu(this, menuItemView);
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ popup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.encryption_choice_none:
+ conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+ item.setChecked(true);
+ break;
+ case R.id.encryption_choice_otr:
+ conversation.setNextEncryption(Message.ENCRYPTION_OTR);
+ item.setChecked(true);
+ break;
+ case R.id.encryption_choice_pgp:
+ if (hasPgp()) {
+ if (conversation.getAccount().getKeys()
+ .has("pgp_signature")) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_PGP);
+ item.setChecked(true);
+ } else {
+ announcePgp(conversation.getAccount(),
+ conversation);
+ }
+ } else {
+ showInstallPgpDialog();
+ }
+ break;
+ default:
+ conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+ break;
+ }
+ xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ fragment.updateChatMsgHint();
+ return true;
+ }
+ });
+ popup.inflate(R.menu.encryption_choices);
+ MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
+ MenuItem none = popup.getMenu().findItem(
+ R.id.encryption_choice_none);
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ otr.setEnabled(false);
+ } else {
+ if (forceEncryption()) {
+ none.setVisible(false);
+ }
+ }
+ switch (conversation.getNextEncryption(forceEncryption())) {
+ case Message.ENCRYPTION_NONE:
+ none.setChecked(true);
+ break;
+ case Message.ENCRYPTION_OTR:
+ otr.setChecked(true);
+ break;
+ case Message.ENCRYPTION_PGP:
+ popup.getMenu().findItem(R.id.encryption_choice_pgp)
+ .setChecked(true);
+ break;
+ default:
+ popup.getMenu().findItem(R.id.encryption_choice_none)
+ .setChecked(true);
+ break;
+ }
+ popup.show();
+ }
+ }
+
+ protected void muteConversationDialog(final Conversation conversation) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.disable_notifications_for_this_conversation);
+ final int[] durations = getResources().getIntArray(
+ R.array.mute_options_durations);
+ builder.setItems(R.array.mute_options_descriptions,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ long till;
+ if (durations[which] == -1) {
+ till = Long.MAX_VALUE;
+ } else {
+ till = SystemClock.elapsedRealtime()
+ + (durations[which] * 1000);
+ }
+ conversation.setMutedTill(till);
+ ConversationActivity.this.xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.updateMessages();
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ protected ConversationFragment swapConversationFragment() {
+ ConversationFragment selectedFragment = new ConversationFragment();
+ if (!isFinishing()) {
+
+ FragmentTransaction transaction = getFragmentManager()
+ .beginTransaction();
+ transaction.replace(R.id.selected_conversation, selectedFragment,
+ "conversation");
+ try {
+ transaction.commitAllowingStateLoss();
+ } catch (IllegalStateException e) {
+ return selectedFragment;
+ }
+ }
+ return selectedFragment;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (!isConversationsOverviewVisable()) {
+ showConversationsOverview();
+ return false;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (xmppConnectionServiceBound) {
+ if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION
+ .equals(intent.getType())))) {
+ String convToView = (String) intent.getExtras().get(
+ CONVERSATION);
+ updateConversationList();
+ for (int i = 0; i < conversationList.size(); ++i) {
+ if (conversationList.get(i).getUuid().equals(convToView)) {
+ setSelectedConversation(conversationList.get(i));
+ break;
+ }
+ }
+ paneShouldBeOpen = false;
+ String text = intent.getExtras().getString(TEXT, null);
+ swapConversationFragment().setText(text);
+ }
+ } else {
+ handledViewIntent = false;
+ setIntent(intent);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (this.xmppConnectionServiceBound) {
+ this.onBackendConnected();
+ }
+ if (conversationList.size() >= 1) {
+ this.onConversationUpdate();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnConversationListChangedListener();
+ xmppConnectionService.removeOnAccountListChangedListener();
+ xmppConnectionService.removeOnRosterUpdateListener();
+ xmppConnectionService.getNotificationService().setOpenConversation(
+ null);
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ Conversation conversation = getSelectedConversation();
+ if (conversation != null) {
+ savedInstanceState.putString(STATE_OPEN_CONVERSATION,
+ conversation.getUuid());
+ }
+ savedInstanceState.putBoolean(STATE_PANEL_OPEN,
+ isConversationsOverviewVisable());
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ void onBackendConnected() {
+ this.registerListener();
+ updateConversationList();
+
+ if (xmppConnectionService.getAccounts().size() == 0) {
+ startActivity(new Intent(this, EditAccountActivity.class));
+ } else if (conversationList.size() <= 0) {
+ startActivity(new Intent(this, StartConversationActivity.class));
+ finish();
+ } else if (mOpenConverstaion != null) {
+ selectConversationByUuid(mOpenConverstaion);
+ paneShouldBeOpen = mPanelOpen;
+ if (paneShouldBeOpen) {
+ showConversationsOverview();
+ }
+ swapConversationFragment();
+ mOpenConverstaion = null;
+ } else if (getIntent() != null
+ && VIEW_CONVERSATION.equals(getIntent().getType())) {
+ String uuid = (String) getIntent().getExtras().get(CONVERSATION);
+ String text = getIntent().getExtras().getString(TEXT, null);
+ selectConversationByUuid(uuid);
+ paneShouldBeOpen = false;
+ swapConversationFragment().setText(text);
+ setIntent(null);
+ } else {
+ showConversationsOverview();
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.onBackendConnected();
+ } else {
+ pendingImageUri = null;
+ setSelectedConversation(conversationList.get(0));
+ swapConversationFragment();
+ }
+ }
+
+ if (pendingImageUri != null) {
+ attachImageToConversation(getSelectedConversation(),
+ pendingImageUri);
+ pendingImageUri = null;
+ }
+ ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
+ }
+
+ private void selectConversationByUuid(String uuid) {
+ for (int i = 0; i < conversationList.size(); ++i) {
+ if (conversationList.get(i).getUuid().equals(uuid)) {
+ setSelectedConversation(conversationList.get(i));
+ }
+ }
+ }
+
+ public void registerListener() {
+ xmppConnectionService.setOnConversationListChangedListener(this);
+ xmppConnectionService.setOnAccountListChangedListener(this);
+ xmppConnectionService.setOnRosterUpdateListener(this);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_DECRYPT_PGP) {
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.hideSnackbar();
+ selectedFragment.updateMessages();
+ }
+ } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
+ pendingImageUri = data.getData();
+ if (xmppConnectionServiceBound) {
+ attachImageToConversation(getSelectedConversation(),
+ pendingImageUri);
+ pendingImageUri = null;
+ }
+ } else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
+
+ } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+ attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+ } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+ attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+ } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
+ announcePgp(getSelectedConversation().getAccount(),
+ getSelectedConversation());
+ } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) {
+ // encryptTextMessage();
+ } else if (requestCode == REQUEST_IMAGE_CAPTURE) {
+ if (xmppConnectionServiceBound) {
+ attachImageToConversation(getSelectedConversation(),
+ pendingImageUri);
+ pendingImageUri = null;
+ }
+ Intent intent = new Intent(
+ Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(pendingImageUri);
+ sendBroadcast(intent);
+ } else if (requestCode == REQUEST_RECORD_AUDIO) {
+ attachAudioToConversation(getSelectedConversation(),
+ data.getData());
+ }
+ } else {
+ if (requestCode == REQUEST_IMAGE_CAPTURE) {
+ pendingImageUri = null;
+ }
+ }
+ }
+
+ private void attachAudioToConversation(Conversation conversation, Uri uri) {
+
+ }
+
+ private void attachImageToConversation(Conversation conversation, Uri uri) {
+ prepareImageToast = Toast.makeText(getApplicationContext(),
+ getText(R.string.preparing_image), Toast.LENGTH_LONG);
+ prepareImageToast.show();
+ xmppConnectionService.attachImageToConversation(conversation, uri,
+ new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Message object) {
+ hidePrepareImageToast();
+ ConversationActivity.this.runIntent(pi,
+ ConversationActivity.REQUEST_SEND_PGP_IMAGE);
+ }
+
+ @Override
+ public void success(Message message) {
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+ hidePrepareImageToast();
+ displayErrorDialog(error);
+ }
+ });
+ }
+
+ private void hidePrepareImageToast() {
+ if (prepareImageToast != null) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ prepareImageToast.cancel();
+ }
+ });
+ }
+ }
+
+ public void updateConversationList() {
+ xmppConnectionService
+ .populateWithOrderedConversations(conversationList);
+ listAdapter.notifyDataSetChanged();
+ }
+
+ public void runIntent(PendingIntent pi, int requestCode) {
+ try {
+ this.startIntentSenderForResult(pi.getIntentSender(), requestCode,
+ null, 0, 0, 0);
+ } catch (SendIntentException e1) {
+ }
+ }
+
+ public void encryptTextMessage(Message message) {
+ xmppConnectionService.getPgpEngine().encrypt(message,
+ new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Message message) {
+ ConversationActivity.this.runIntent(pi,
+ ConversationActivity.REQUEST_SEND_MESSAGE);
+ }
+
+ @Override
+ public void success(Message message) {
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+
+ }
+ });
+ }
+
+ public boolean forceEncryption() {
+ return getPreferences().getBoolean("force_encryption", false);
+ }
+
+ public boolean useSendButtonToIndicateStatus() {
+ return getPreferences().getBoolean("send_button_status", false);
+ }
+
+ public boolean indicateReceived() {
+ return getPreferences().getBoolean("indicate_received", false);
+ }
+
+ @Override
+ public void onAccountUpdate() {
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ fragment.updateMessages();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onConversationUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ updateConversationList();
+ if (paneShouldBeOpen) {
+ if (conversationList.size() >= 1) {
+ swapConversationFragment();
+ } else {
+ startActivity(new Intent(getApplicationContext(),
+ StartConversationActivity.class));
+ finish();
+ }
+ }
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (selectedFragment != null) {
+ selectedFragment.updateMessages();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRosterUpdate() {
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+ .findFragmentByTag("conversation");
+ if (fragment != null) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ fragment.updateMessages();
+ }
+ });
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
new file mode 100644
index 00000000..0e71801b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -0,0 +1,781 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.EditMessage.OnEnterPressed;
+import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
+import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
+import eu.siacs.conversations.ui.adapter.MessageAdapter;
+import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
+import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
+import eu.siacs.conversations.utils.UIHelper;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Selection;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.AbsListView;
+
+import android.widget.ListView;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class ConversationFragment extends Fragment {
+
+ protected Conversation conversation;
+ protected ListView messagesView;
+ protected LayoutInflater inflater;
+ protected List<Message> messageList = new ArrayList<Message>();
+ protected MessageAdapter messageListAdapter;
+ protected Contact contact;
+
+ protected String queuedPqpMessage = null;
+
+ private EditMessage mEditMessage;
+ private ImageButton mSendButton;
+ private String pastedText = null;
+ private RelativeLayout snackbar;
+ private TextView snackbarMessage;
+ private TextView snackbarAction;
+
+ private boolean messagesLoaded = false;
+
+ private IntentSender askForPassphraseIntent = null;
+
+ private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>();
+ private boolean mDecryptJobRunning = false;
+
+ private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEND) {
+ InputMethodManager imm = (InputMethodManager) v.getContext()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ sendMessage();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ private OnClickListener mSendButtonListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ sendMessage();
+ }
+ };
+ protected OnClickListener clickToDecryptListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (activity.hasPgp() && askForPassphraseIntent != null) {
+ try {
+ getActivity().startIntentSenderForResult(
+ askForPassphraseIntent,
+ ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
+ 0, 0);
+ } catch (SendIntentException e) {
+ //
+ }
+ }
+ }
+ };
+
+ private OnClickListener clickToMuc = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(),
+ ConferenceDetailsActivity.class);
+ intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
+ intent.putExtra("uuid", conversation.getUuid());
+ startActivity(intent);
+ }
+ };
+
+ private OnClickListener leaveMuc = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.endConversation(conversation);
+ }
+ };
+
+ private OnClickListener joinMuc = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.xmppConnectionService.joinMuc(conversation);
+ }
+ };
+
+ private OnClickListener enterPassword = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ MucOptions muc = conversation.getMucOptions();
+ String password = muc.getPassword();
+ if (password == null) {
+ password = "";
+ }
+ activity.quickPasswordEdit(password, new OnValueEdited() {
+
+ @Override
+ public void onValueEdited(String value) {
+ activity.xmppConnectionService.providePasswordForMuc(
+ conversation, value);
+ }
+ });
+ }
+ };
+
+ private OnScrollListener mOnScrollListener = new OnScrollListener() {
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem,
+ int visibleItemCount, int totalItemCount) {
+ if (firstVisibleItem == 0 && messagesLoaded) {
+ long timestamp = messageList.get(0).getTimeSent();
+ messagesLoaded = false;
+ int size = activity.xmppConnectionService.loadMoreMessages(
+ conversation, timestamp);
+ messageList.clear();
+ messageList.addAll(conversation.getMessages());
+ updateStatusMessages();
+ messageListAdapter.notifyDataSetChanged();
+ if (size != 0) {
+ messagesLoaded = true;
+ }
+ messagesView.setSelectionFromTop(size + 1, 0);
+ }
+ }
+ };
+
+ private ConversationActivity activity;
+
+ private void sendMessage() {
+ if (this.conversation == null) {
+ return;
+ }
+ if (mEditMessage.getText().length() < 1) {
+ if (this.conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.setNextPresence(null);
+ updateChatMsgHint();
+ }
+ return;
+ }
+ Message message = new Message(conversation, mEditMessage.getText()
+ .toString(), conversation.getNextEncryption(activity
+ .forceEncryption()));
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation.getNextPresence() != null) {
+ message.setPresence(conversation.getNextPresence());
+ message.setType(Message.TYPE_PRIVATE);
+ conversation.setNextPresence(null);
+ }
+ }
+ if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
+ sendOtrMessage(message);
+ } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
+ sendPgpMessage(message);
+ } else {
+ sendPlainTextMessage(message);
+ }
+ }
+
+ public void updateChatMsgHint() {
+ if (conversation.getMode() == Conversation.MODE_MULTI
+ && conversation.getNextPresence() != null) {
+ this.mEditMessage.setHint(getString(
+ R.string.send_private_message_to,
+ conversation.getNextPresence()));
+ } else {
+ switch (conversation.getNextEncryption(activity.forceEncryption())) {
+ case Message.ENCRYPTION_NONE:
+ mEditMessage
+ .setHint(getString(R.string.send_plain_text_message));
+ break;
+ case Message.ENCRYPTION_OTR:
+ mEditMessage.setHint(getString(R.string.send_otr_message));
+ break;
+ case Message.ENCRYPTION_PGP:
+ mEditMessage.setHint(getString(R.string.send_pgp_message));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.fragment_conversation,
+ container, false);
+ mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
+ mEditMessage.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.hideConversationsOverview();
+ }
+ });
+ mEditMessage.setOnEditorActionListener(mEditorActionListener);
+ mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
+
+ @Override
+ public void onEnterPressed() {
+ sendMessage();
+ }
+ });
+
+ mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
+ mSendButton.setOnClickListener(this.mSendButtonListener);
+
+ snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
+ snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
+ snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
+
+ messagesView = (ListView) view.findViewById(R.id.messages_view);
+ messagesView.setOnScrollListener(mOnScrollListener);
+ messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
+ messageListAdapter = new MessageAdapter(
+ (ConversationActivity) getActivity(), this.messageList);
+ messageListAdapter
+ .setOnContactPictureClicked(new OnContactPictureClicked() {
+
+ @Override
+ public void onContactPictureClicked(Message message) {
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ if (message.getPresence() != null) {
+ highlightInConference(message.getPresence());
+ } else {
+ highlightInConference(message
+ .getCounterpart());
+ }
+ } else {
+ Contact contact = message.getConversation()
+ .getContact();
+ if (contact.showInRoster()) {
+ activity.switchToContactDetails(contact);
+ } else {
+ activity.showAddToRosterDialog(message
+ .getConversation());
+ }
+ }
+ }
+ }
+ });
+ messageListAdapter
+ .setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
+
+ @Override
+ public void onContactPictureLongClicked(Message message) {
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ if (message.getPresence() != null) {
+ privateMessageWith(message.getPresence());
+ } else {
+ privateMessageWith(message.getCounterpart());
+ }
+ }
+ }
+ }
+ });
+ messagesView.setAdapter(messageListAdapter);
+
+ return view;
+ }
+
+ protected void privateMessageWith(String counterpart) {
+ this.mEditMessage.setText("");
+ this.conversation.setNextPresence(counterpart);
+ updateChatMsgHint();
+ }
+
+ protected void highlightInConference(String nick) {
+ String oldString = mEditMessage.getText().toString().trim();
+ if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
+ mEditMessage.getText().insert(0, nick + ": ");
+ } else {
+ if (mEditMessage.getText().charAt(
+ mEditMessage.getSelectionStart() - 1) != ' ') {
+ nick = " " + nick;
+ }
+ mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
+ nick + " ");
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ this.activity = (ConversationActivity) getActivity();
+ if (activity.xmppConnectionServiceBound) {
+ this.onBackendConnected();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ mDecryptJobRunning = false;
+ super.onStop();
+ if (this.conversation != null) {
+ this.conversation.setNextMessage(mEditMessage.getText().toString());
+ }
+ }
+
+ public void onBackendConnected() {
+ this.activity = (ConversationActivity) getActivity();
+ this.conversation = activity.getSelectedConversation();
+ if (this.conversation == null) {
+ return;
+ }
+ String oldString = conversation.getNextMessage().trim();
+ if (this.pastedText == null) {
+ this.mEditMessage.setText(oldString);
+ } else {
+
+ if (oldString.isEmpty()) {
+ mEditMessage.setText(pastedText);
+ } else {
+ mEditMessage.setText(oldString + " " + pastedText);
+ }
+ pastedText = null;
+ }
+ int position = mEditMessage.length();
+ Editable etext = mEditMessage.getText();
+ Selection.setSelection(etext, position);
+ if (activity.isConversationsOverviewHideable()) {
+ if (!activity.shouldPaneBeOpen()) {
+ activity.hideConversationsOverview();
+ activity.openConversation(conversation);
+ }
+ }
+ if (this.conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.setNextPresence(null);
+ }
+ updateMessages();
+ }
+
+ public void updateMessages() {
+ if (getView() == null) {
+ return;
+ }
+ hideSnackbar();
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ if (this.conversation != null) {
+ final Contact contact = this.conversation.getContact();
+ if (this.conversation.isMuted()) {
+ showSnackbar(R.string.notifications_disabled, R.string.enable,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ conversation.setMutedTill(0);
+ activity.xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ updateMessages();
+ }
+ });
+ } else if (!contact.showInRoster()
+ && contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ showSnackbar(R.string.contact_added_you, R.string.add_back,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.xmppConnectionService
+ .createContact(contact);
+ activity.switchToContactDetails(contact);
+ }
+ });
+ }
+ for (Message message : this.conversation.getMessages()) {
+ if ((message.getEncryption() == Message.ENCRYPTION_PGP)
+ && ((message.getStatus() == Message.STATUS_RECEIVED) || (message
+ .getStatus() == Message.STATUS_SEND))) {
+ if (!mEncryptedMessages.contains(message)) {
+ mEncryptedMessages.add(message);
+ }
+ }
+ }
+ decryptNext();
+ this.messageList.clear();
+ if (this.conversation.getMessages().size() == 0) {
+ messagesLoaded = false;
+ } else {
+ this.messageList.addAll(this.conversation.getMessages());
+ messagesLoaded = true;
+ updateStatusMessages();
+ }
+ this.messageListAdapter.notifyDataSetChanged();
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ if (messageList.size() >= 1) {
+ makeFingerprintWarning(conversation.getLatestEncryption());
+ }
+ } else {
+ if (!conversation.getMucOptions().online()
+ && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ int error = conversation.getMucOptions().getError();
+ switch (error) {
+ case MucOptions.ERROR_NICK_IN_USE:
+ showSnackbar(R.string.nick_in_use, R.string.edit,
+ clickToMuc);
+ break;
+ case MucOptions.ERROR_ROOM_NOT_FOUND:
+ showSnackbar(R.string.conference_not_found,
+ R.string.leave, leaveMuc);
+ break;
+ case MucOptions.ERROR_PASSWORD_REQUIRED:
+ showSnackbar(R.string.conference_requires_password,
+ R.string.enter_password, enterPassword);
+ break;
+ case MucOptions.ERROR_BANNED:
+ showSnackbar(R.string.conference_banned,
+ R.string.leave, leaveMuc);
+ break;
+ case MucOptions.ERROR_MEMBERS_ONLY:
+ showSnackbar(R.string.conference_members_only,
+ R.string.leave, leaveMuc);
+ break;
+ case MucOptions.KICKED_FROM_ROOM:
+ showSnackbar(R.string.conference_kicked, R.string.join,
+ joinMuc);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ getActivity().invalidateOptionsMenu();
+ updateChatMsgHint();
+ if (!activity.shouldPaneBeOpen()) {
+ activity.xmppConnectionService.markRead(conversation, true);
+ activity.updateConversationList();
+ }
+ this.updateSendButton();
+ }
+ }
+
+ private void decryptNext() {
+ Message next = this.mEncryptedMessages.peek();
+ PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
+
+ if (next != null && engine != null && !mDecryptJobRunning) {
+ mDecryptJobRunning = true;
+ engine.decrypt(next, new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message message) {
+ mDecryptJobRunning = false;
+ askForPassphraseIntent = pi.getIntentSender();
+ showSnackbar(R.string.openpgp_messages_found,
+ R.string.decrypt, clickToDecryptListener);
+ }
+
+ @Override
+ public void success(Message message) {
+ mDecryptJobRunning = false;
+ mEncryptedMessages.remove();
+ activity.xmppConnectionService.updateMessage(message);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+ message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
+ mDecryptJobRunning = false;
+ mEncryptedMessages.remove();
+ activity.xmppConnectionService.updateConversationUi();
+ }
+ });
+ }
+ }
+
+ private void messageSent() {
+ int size = this.messageList.size();
+ messagesView.setSelection(size - 1);
+ mEditMessage.setText("");
+ updateChatMsgHint();
+ }
+
+ public void updateSendButton() {
+ Conversation c = this.conversation;
+ if (activity.useSendButtonToIndicateStatus() && c != null
+ && c.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ if (c.getMode() == Conversation.MODE_SINGLE) {
+ switch (c.getContact().getMostAvailableStatus()) {
+ case Presences.CHAT:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_online);
+ break;
+ case Presences.ONLINE:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_online);
+ break;
+ case Presences.AWAY:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_away);
+ break;
+ case Presences.XA:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_away);
+ break;
+ case Presences.DND:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_dnd);
+ break;
+ default:
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ break;
+ }
+ } else if (c.getMode() == Conversation.MODE_MULTI) {
+ if (c.getMucOptions().online()) {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_online);
+ } else {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ }
+ } else {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ }
+ } else {
+ this.mSendButton
+ .setImageResource(R.drawable.ic_action_send_now_offline);
+ }
+ }
+
+ protected void updateStatusMessages() {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ for (int i = this.messageList.size() - 1; i >= 0; --i) {
+ if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
+ return;
+ } else {
+ if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
+ this.messageList.add(i + 1,
+ Message.createStatusMessage(conversation));
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ protected void makeFingerprintWarning(int latestEncryption) {
+ Set<String> knownFingerprints = conversation.getContact()
+ .getOtrFingerprints();
+ if ((latestEncryption == Message.ENCRYPTION_OTR)
+ && (conversation.hasValidOtrSession()
+ && (!conversation.isMuted())
+ && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
+ .contains(conversation.getOtrFingerprint())))) {
+ showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (conversation.getOtrFingerprint() != null) {
+ AlertDialog dialog = UIHelper
+ .getVerifyFingerprintDialog(
+ (ConversationActivity) getActivity(),
+ conversation, snackbar);
+ dialog.show();
+ }
+ }
+ });
+ }
+ }
+
+ protected void showSnackbar(int message, int action,
+ OnClickListener clickListener) {
+ snackbar.setVisibility(View.VISIBLE);
+ snackbar.setOnClickListener(null);
+ snackbarMessage.setText(message);
+ snackbarMessage.setOnClickListener(null);
+ snackbarAction.setText(action);
+ snackbarAction.setOnClickListener(clickListener);
+ }
+
+ protected void hideSnackbar() {
+ snackbar.setVisibility(View.GONE);
+ }
+
+ protected void sendPlainTextMessage(Message message) {
+ ConversationActivity activity = (ConversationActivity) getActivity();
+ activity.xmppConnectionService.sendMessage(message);
+ messageSent();
+ }
+
+ protected void sendPgpMessage(final Message message) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ final Contact contact = message.getConversation().getContact();
+ if (activity.hasPgp()) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ if (contact.getPgpKeyId() != 0) {
+ xmppService.getPgpEngine().hasKey(contact,
+ new UiCallback<Contact>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Contact contact) {
+ activity.runIntent(
+ pi,
+ ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
+ }
+
+ @Override
+ public void success(Contact contact) {
+ messageSent();
+ activity.encryptTextMessage(message);
+ }
+
+ @Override
+ public void error(int error, Contact contact) {
+
+ }
+ });
+
+ } else {
+ showNoPGPKeyDialog(false,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_NONE);
+ xmppService.databaseBackend
+ .updateConversation(conversation);
+ message.setEncryption(Message.ENCRYPTION_NONE);
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+ });
+ }
+ } else {
+ if (conversation.getMucOptions().pgpKeysInUse()) {
+ if (!conversation.getMucOptions().everybodyHasKeys()) {
+ Toast warning = Toast
+ .makeText(getActivity(),
+ R.string.missing_public_keys,
+ Toast.LENGTH_LONG);
+ warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
+ warning.show();
+ }
+ activity.encryptTextMessage(message);
+ messageSent();
+ } else {
+ showNoPGPKeyDialog(true,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_NONE);
+ message.setEncryption(Message.ENCRYPTION_NONE);
+ xmppService.databaseBackend
+ .updateConversation(conversation);
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+ });
+ }
+ }
+ } else {
+ activity.showInstallPgpDialog();
+ }
+ }
+
+ public void showNoPGPKeyDialog(boolean plural,
+ DialogInterface.OnClickListener listener) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ if (plural) {
+ builder.setTitle(getString(R.string.no_pgp_keys));
+ builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
+ } else {
+ builder.setTitle(getString(R.string.no_pgp_key));
+ builder.setMessage(getText(R.string.contact_has_no_pgp_key));
+ }
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.send_unencrypted),
+ listener);
+ builder.create().show();
+ }
+
+ protected void sendOtrMessage(final Message message) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ if (conversation.hasValidOtrSession()) {
+ activity.xmppConnectionService.sendMessage(message);
+ messageSent();
+ } else {
+ activity.selectPresence(message.getConversation(),
+ new OnPresenceSelected() {
+
+ @Override
+ public void onPresenceSelected() {
+ message.setPresence(conversation.getNextPresence());
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+ });
+ }
+ }
+
+ public void setText(String text) {
+ this.pastedText = text;
+ }
+
+ public void clearInputField() {
+ this.mEditMessage.setText("");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
new file mode 100644
index 00000000..1543d740
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -0,0 +1,423 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.Validator;
+import eu.siacs.conversations.xmpp.XmppConnection.Features;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class EditAccountActivity extends XmppActivity {
+
+ private AutoCompleteTextView mAccountJid;
+ private EditText mPassword;
+ private EditText mPasswordConfirm;
+ private CheckBox mRegisterNew;
+ private Button mCancelButton;
+ private Button mSaveButton;
+
+ private LinearLayout mStats;
+ private TextView mServerInfoSm;
+ private TextView mServerInfoCarbons;
+ private TextView mServerInfoPep;
+ private TextView mSessionEst;
+ private TextView mOtrFingerprint;
+ private RelativeLayout mOtrFingerprintBox;
+ private ImageButton mOtrFingerprintToClipboardButton;
+
+ private String jidToEdit;
+ private Account mAccount;
+
+ private boolean mFetchingAvatar = false;
+
+ private OnClickListener mSaveButtonClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_DISABLED) {
+ mAccount.setOption(Account.OPTION_DISABLED, false);
+ xmppConnectionService.updateAccount(mAccount);
+ return;
+ }
+ if (!Validator.isValidJid(mAccountJid.getText().toString())) {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ mAccountJid.requestFocus();
+ return;
+ }
+ boolean registerNewAccount = mRegisterNew.isChecked();
+ String[] jidParts = mAccountJid.getText().toString().split("@");
+ String username = jidParts[0];
+ String server;
+ if (jidParts.length >= 2) {
+ server = jidParts[1];
+ } else {
+ server = "";
+ }
+ String password = mPassword.getText().toString();
+ String passwordConfirm = mPasswordConfirm.getText().toString();
+ if (registerNewAccount) {
+ if (!password.equals(passwordConfirm)) {
+ mPasswordConfirm
+ .setError(getString(R.string.passwords_do_not_match));
+ mPasswordConfirm.requestFocus();
+ return;
+ }
+ }
+ if (mAccount != null) {
+ mAccount.setPassword(password);
+ mAccount.setUsername(username);
+ mAccount.setServer(server);
+ mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+ xmppConnectionService.updateAccount(mAccount);
+ } else {
+ if (xmppConnectionService.findAccountByJid(mAccountJid
+ .getText().toString()) != null) {
+ mAccountJid
+ .setError(getString(R.string.account_already_exists));
+ mAccountJid.requestFocus();
+ return;
+ }
+ mAccount = new Account(username, server, password);
+ mAccount.setOption(Account.OPTION_USETLS, true);
+ mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
+ mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+ xmppConnectionService.createAccount(mAccount);
+ }
+ if (jidToEdit != null) {
+ finish();
+ } else {
+ updateSaveButton();
+ updateAccountInformation();
+ }
+
+ }
+ };
+ private OnClickListener mCancelButtonClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ };
+ private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() {
+
+ @Override
+ public void onAccountUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (mAccount != null
+ && mAccount.getStatus() != Account.STATUS_ONLINE
+ && mFetchingAvatar) {
+ startActivity(new Intent(getApplicationContext(),
+ ManageAccountActivity.class));
+ finish();
+ } else if (jidToEdit == null && mAccount != null
+ && mAccount.getStatus() == Account.STATUS_ONLINE) {
+ if (!mFetchingAvatar) {
+ mFetchingAvatar = true;
+ xmppConnectionService.checkForAvatar(mAccount,
+ mAvatarFetchCallback);
+ }
+ } else {
+ updateSaveButton();
+ }
+ if (mAccount != null) {
+ updateAccountInformation();
+ }
+ }
+ });
+ }
+ };
+ private UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+
+ @Override
+ public void success(Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+
+ @Override
+ public void error(int errorCode, Avatar avatar) {
+ finishInitialSetup(avatar);
+ }
+ };
+ private KnownHostsAdapter mKnownHostsAdapter;
+ private TextWatcher mTextWatcher = new TextWatcher() {
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ updateSaveButton();
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+
+ }
+ };
+
+ protected void finishInitialSetup(final Avatar avatar) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ Intent intent;
+ if (avatar != null) {
+ intent = new Intent(getApplicationContext(),
+ StartConversationActivity.class);
+ } else {
+ intent = new Intent(getApplicationContext(),
+ PublishProfilePictureActivity.class);
+ intent.putExtra("account", mAccount.getJid());
+ intent.putExtra("setup", true);
+ }
+ startActivity(intent);
+ finish();
+ }
+ });
+ }
+
+ protected boolean inputDataDiffersFromAccount() {
+ if (mAccount == null) {
+ return true;
+ } else {
+ return (!mAccount.getJid().equals(mAccountJid.getText().toString()))
+ || (!mAccount.getPassword().equals(
+ mPassword.getText().toString()) || mAccount
+ .isOptionSet(Account.OPTION_REGISTER) != mRegisterNew
+ .isChecked());
+ }
+ }
+
+ protected void updateSaveButton() {
+ if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_CONNECTING) {
+ this.mSaveButton.setEnabled(false);
+ this.mSaveButton.setTextColor(getSecondaryTextColor());
+ this.mSaveButton.setText(R.string.account_status_connecting);
+ } else if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_DISABLED) {
+ this.mSaveButton.setEnabled(true);
+ this.mSaveButton.setTextColor(getPrimaryTextColor());
+ this.mSaveButton.setText(R.string.enable);
+ } else {
+ this.mSaveButton.setEnabled(true);
+ this.mSaveButton.setTextColor(getPrimaryTextColor());
+ if (jidToEdit != null) {
+ if (mAccount != null
+ && mAccount.getStatus() == Account.STATUS_ONLINE) {
+ this.mSaveButton.setText(R.string.save);
+ if (!accountInfoEdited()) {
+ this.mSaveButton.setEnabled(false);
+ this.mSaveButton.setTextColor(getSecondaryTextColor());
+ }
+ } else {
+ this.mSaveButton.setText(R.string.connect);
+ }
+ } else {
+ this.mSaveButton.setText(R.string.next);
+ }
+ }
+ }
+
+ protected boolean accountInfoEdited() {
+ return (!this.mAccount.getJid().equals(
+ this.mAccountJid.getText().toString()))
+ || (!this.mAccount.getPassword().equals(
+ this.mPassword.getText().toString()));
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_edit_account);
+ this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid);
+ this.mAccountJid.addTextChangedListener(this.mTextWatcher);
+ this.mPassword = (EditText) findViewById(R.id.account_password);
+ this.mPassword.addTextChangedListener(this.mTextWatcher);
+ this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm);
+ this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new);
+ this.mStats = (LinearLayout) findViewById(R.id.stats);
+ this.mSessionEst = (TextView) findViewById(R.id.session_est);
+ this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons);
+ this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
+ this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
+ this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
+ this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
+ this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
+ this.mSaveButton = (Button) findViewById(R.id.save_button);
+ this.mCancelButton = (Button) findViewById(R.id.cancel_button);
+ this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
+ this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener);
+ this.mRegisterNew
+ .setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (isChecked) {
+ mPasswordConfirm.setVisibility(View.VISIBLE);
+ } else {
+ mPasswordConfirm.setVisibility(View.GONE);
+ }
+ updateSaveButton();
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getIntent() != null) {
+ this.jidToEdit = getIntent().getStringExtra("jid");
+ if (this.jidToEdit != null) {
+ this.mRegisterNew.setVisibility(View.GONE);
+ getActionBar().setTitle(jidToEdit);
+ } else {
+ getActionBar().setTitle(R.string.action_add_account);
+ }
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnAccountListChangedListener();
+ }
+ super.onStop();
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ this.mKnownHostsAdapter = new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1,
+ xmppConnectionService.getKnownHosts());
+ this.xmppConnectionService
+ .setOnAccountListChangedListener(this.mOnAccountUpdateListener);
+ if (this.jidToEdit != null) {
+ this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
+ updateAccountInformation();
+ } else if (this.xmppConnectionService.getAccounts().size() == 0) {
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setDisplayShowHomeEnabled(false);
+ this.mCancelButton.setEnabled(false);
+ this.mCancelButton.setTextColor(getSecondaryTextColor());
+ }
+ this.mAccountJid.setAdapter(this.mKnownHostsAdapter);
+ updateSaveButton();
+ }
+
+ private void updateAccountInformation() {
+ this.mAccountJid.setText(this.mAccount.getJid());
+ this.mPassword.setText(this.mAccount.getPassword());
+ if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
+ this.mRegisterNew.setVisibility(View.VISIBLE);
+ this.mRegisterNew.setChecked(true);
+ this.mPasswordConfirm.setText(this.mAccount.getPassword());
+ } else {
+ this.mRegisterNew.setVisibility(View.GONE);
+ this.mRegisterNew.setChecked(false);
+ }
+ if (this.mAccount.getStatus() == Account.STATUS_ONLINE
+ && !this.mFetchingAvatar) {
+ this.mStats.setVisibility(View.VISIBLE);
+ this.mSessionEst.setText(UIHelper.readableTimeDifference(
+ getApplicationContext(), this.mAccount.getXmppConnection()
+ .getLastSessionEstablished()));
+ Features features = this.mAccount.getXmppConnection().getFeatures();
+ if (features.carbons()) {
+ this.mServerInfoCarbons.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoCarbons
+ .setText(R.string.server_info_unavailable);
+ }
+ if (features.sm()) {
+ this.mServerInfoSm.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoSm.setText(R.string.server_info_unavailable);
+ }
+ if (features.pubsub()) {
+ this.mServerInfoPep.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoPep.setText(R.string.server_info_unavailable);
+ }
+ final String fingerprint = this.mAccount
+ .getOtrFingerprint(xmppConnectionService);
+ if (fingerprint != null) {
+ this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
+ this.mOtrFingerprint.setText(fingerprint);
+ this.mOtrFingerprintToClipboardButton
+ .setVisibility(View.VISIBLE);
+ this.mOtrFingerprintToClipboardButton
+ .setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+
+ if (OtrFingerprintToClipBoard(fingerprint)) {
+ Toast.makeText(
+ EditAccountActivity.this,
+ R.string.toast_message_otr_fingerprint,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ } else {
+ this.mOtrFingerprintBox.setVisibility(View.GONE);
+ }
+ } else {
+ if (this.mAccount.errorStatus()) {
+ this.mAccountJid.setError(getString(this.mAccount
+ .getReadableStatusId()));
+ this.mAccountJid.requestFocus();
+ }
+ this.mStats.setVisibility(View.GONE);
+ }
+ }
+
+ private boolean OtrFingerprintToClipBoard(String fingerprint) {
+ ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ String label = getResources().getString(R.string.otr_fingerprint);
+ if (mClipBoardManager != null) {
+ ClipData mClipData = ClipData.newPlainText(label, fingerprint);
+ mClipBoardManager.setPrimaryClip(mClipData);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java
new file mode 100644
index 00000000..f8302050
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java
@@ -0,0 +1,39 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+public class EditMessage extends EditText {
+
+ public EditMessage(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EditMessage(Context context) {
+ super(context);
+ }
+
+ protected OnEnterPressed mOnEnterPressed;
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ if (mOnEnterPressed != null) {
+ mOnEnterPressed.onEnterPressed();
+ }
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public void setOnEnterPressedListener(OnEnterPressed listener) {
+ this.mOnEnterPressed = listener;
+ }
+
+ public interface OnEnterPressed {
+ public void onEnterPressed();
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
new file mode 100644
index 00000000..5b5b0608
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -0,0 +1,217 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+public class ManageAccountActivity extends XmppActivity {
+
+ protected Account selectedAccount = null;
+
+ protected List<Account> accountList = new ArrayList<Account>();
+ protected ListView accountListView;
+ protected AccountAdapter mAccountAdapter;
+ protected OnAccountUpdate accountChanged = new OnAccountUpdate() {
+
+ @Override
+ public void onAccountUpdate() {
+ accountList.clear();
+ accountList.addAll(xmppConnectionService.getAccounts());
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ mAccountAdapter.notifyDataSetChanged();
+ }
+ });
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.manage_accounts);
+
+ accountListView = (ListView) findViewById(R.id.account_list);
+ this.mAccountAdapter = new AccountAdapter(this, accountList);
+ accountListView.setAdapter(this.mAccountAdapter);
+ accountListView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View view,
+ int position, long arg3) {
+ switchToAccount(accountList.get(position));
+ }
+ });
+ registerForContextMenu(accountListView);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ManageAccountActivity.this.getMenuInflater().inflate(
+ R.menu.manageaccounts_context, menu);
+ AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ this.selectedAccount = accountList.get(acmi.position);
+ if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) {
+ menu.findItem(R.id.mgmt_account_disable).setVisible(false);
+ menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
+ menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
+ } else {
+ menu.findItem(R.id.mgmt_account_enable).setVisible(false);
+ }
+ menu.setHeaderTitle(this.selectedAccount.getJid());
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnAccountListChangedListener();
+ }
+ super.onStop();
+ }
+
+ @Override
+ void onBackendConnected() {
+ xmppConnectionService.setOnAccountListChangedListener(accountChanged);
+ this.accountList.clear();
+ this.accountList.addAll(xmppConnectionService.getAccounts());
+ mAccountAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.manageaccounts, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.mgmt_account_publish_avatar:
+ publishAvatar(selectedAccount);
+ return true;
+ case R.id.mgmt_account_disable:
+ disableAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_enable:
+ enableAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_delete:
+ deleteAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_announce_pgp:
+ publishOpenPGPPublicKey(selectedAccount);
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add_account:
+ startActivity(new Intent(getApplicationContext(),
+ EditAccountActivity.class));
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ if (xmppConnectionService.getConversations().size() == 0) {
+ Intent contactsIntent = new Intent(this,
+ StartConversationActivity.class);
+ contactsIntent.setFlags(
+ // if activity exists in stack, pop the stack and go back to it
+ Intent.FLAG_ACTIVITY_CLEAR_TOP |
+ // otherwise, make a new task for it
+ Intent.FLAG_ACTIVITY_NEW_TASK |
+ // don't use the new activity animation; finish
+ // animation runs instead
+ Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(contactsIntent);
+ finish();
+ return true;
+ } else {
+ return super.onNavigateUp();
+ }
+ }
+
+ private void publishAvatar(Account account) {
+ Intent intent = new Intent(getApplicationContext(),
+ PublishProfilePictureActivity.class);
+ intent.putExtra("account", account.getJid());
+ startActivity(intent);
+ }
+
+ private void disableAccount(Account account) {
+ account.setOption(Account.OPTION_DISABLED, true);
+ xmppConnectionService.updateAccount(account);
+ }
+
+ private void enableAccount(Account account) {
+ account.setOption(Account.OPTION_DISABLED, false);
+ xmppConnectionService.updateAccount(account);
+ }
+
+ private void publishOpenPGPPublicKey(Account account) {
+ if (ManageAccountActivity.this.hasPgp()) {
+ announcePgp(account, null);
+ } else {
+ this.showInstallPgpDialog();
+ }
+ }
+
+ private void deleteAccount(final Account account) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ ManageAccountActivity.this);
+ builder.setTitle(getString(R.string.mgmt_account_are_you_sure));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text));
+ builder.setPositiveButton(getString(R.string.delete),
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ xmppConnectionService.deleteAccount(account);
+ selectedAccount = null;
+ }
+ });
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.create().show();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_ANNOUNCE_PGP) {
+ announcePgp(selectedAccount, null);
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
new file mode 100644
index 00000000..6aa40c41
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
@@ -0,0 +1,242 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class PublishProfilePictureActivity extends XmppActivity {
+
+ private static final int REQUEST_CHOOSE_FILE = 0xac23;
+
+ private ImageView avatar;
+ private TextView accountTextView;
+ private TextView hintOrWarning;
+ private TextView secondaryHint;
+ private Button cancelButton;
+ private Button publishButton;
+
+ private Uri avatarUri;
+ private Uri defaultUri;
+
+ private Account account;
+
+ private boolean support = false;
+
+ private boolean mInitialAccountSetup;
+
+ private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() {
+
+ @Override
+ public void success(Avatar object) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (mInitialAccountSetup) {
+ startActivity(new Intent(getApplicationContext(),
+ StartConversationActivity.class));
+ }
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void error(final int errorCode, Avatar object) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ hintOrWarning.setText(errorCode);
+ hintOrWarning.setTextColor(getWarningTextColor());
+ publishButton.setText(R.string.publish);
+ enablePublishButton();
+ }
+ });
+
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Avatar object) {
+ }
+ };
+
+ private OnLongClickListener backToDefaultListener = new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ avatarUri = defaultUri;
+ loadImageIntoPreview(defaultUri);
+ return true;
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_publish_profile_picture);
+ this.avatar = (ImageView) findViewById(R.id.account_image);
+ this.cancelButton = (Button) findViewById(R.id.cancel_button);
+ this.publishButton = (Button) findViewById(R.id.publish_button);
+ this.accountTextView = (TextView) findViewById(R.id.account);
+ this.hintOrWarning = (TextView) findViewById(R.id.hint_or_warning);
+ this.secondaryHint = (TextView) findViewById(R.id.secondary_hint);
+ this.publishButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (avatarUri != null) {
+ publishButton.setText(R.string.publishing);
+ disablePublishButton();
+ xmppConnectionService.publishAvatar(account, avatarUri,
+ avatarPublication);
+ }
+ }
+ });
+ this.cancelButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mInitialAccountSetup) {
+ startActivity(new Intent(getApplicationContext(),
+ StartConversationActivity.class));
+ }
+ finish();
+ }
+ });
+ this.avatar.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent attachFileIntent = new Intent();
+ attachFileIntent.setType("image/*");
+ attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
+ Intent chooser = Intent.createChooser(attachFileIntent,
+ getString(R.string.attach_file));
+ startActivityForResult(chooser, REQUEST_CHOOSE_FILE);
+ }
+ });
+ this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext());
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_CHOOSE_FILE) {
+ this.avatarUri = data.getData();
+ if (xmppConnectionServiceBound) {
+ loadImageIntoPreview(this.avatarUri);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ if (getIntent() != null) {
+ String jid = getIntent().getStringExtra("account");
+ if (jid != null) {
+ this.account = xmppConnectionService.findAccountByJid(jid);
+ if (this.account.getXmppConnection() != null) {
+ this.support = this.account.getXmppConnection()
+ .getFeatures().pubsub();
+ }
+ if (this.avatarUri == null) {
+ if (this.account.getAvatar() != null
+ || this.defaultUri == null) {
+ this.avatar.setImageBitmap(avatarService().get(account,
+ getPixel(194)));
+ if (this.defaultUri != null) {
+ this.avatar
+ .setOnLongClickListener(this.backToDefaultListener);
+ } else {
+ this.secondaryHint.setVisibility(View.INVISIBLE);
+ }
+ if (!support) {
+ this.hintOrWarning
+ .setTextColor(getWarningTextColor());
+ this.hintOrWarning
+ .setText(R.string.error_publish_avatar_no_server_support);
+ }
+ } else {
+ this.avatarUri = this.defaultUri;
+ loadImageIntoPreview(this.defaultUri);
+ this.secondaryHint.setVisibility(View.INVISIBLE);
+ }
+ } else {
+ loadImageIntoPreview(avatarUri);
+ }
+ this.accountTextView.setText(this.account.getJid());
+ }
+ }
+
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getIntent() != null) {
+ this.mInitialAccountSetup = getIntent().getBooleanExtra("setup",
+ false);
+ }
+ if (this.mInitialAccountSetup) {
+ this.cancelButton.setText(R.string.skip);
+ }
+ }
+
+ protected void loadImageIntoPreview(Uri uri) {
+ Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare(
+ uri, 384);
+ if (bm == null) {
+ disablePublishButton();
+ this.hintOrWarning.setTextColor(getWarningTextColor());
+ this.hintOrWarning
+ .setText(R.string.error_publish_avatar_converting);
+ return;
+ }
+ this.avatar.setImageBitmap(bm);
+ if (support) {
+ enablePublishButton();
+ this.publishButton.setText(R.string.publish);
+ this.hintOrWarning.setText(R.string.publish_avatar_explanation);
+ this.hintOrWarning.setTextColor(getPrimaryTextColor());
+ } else {
+ disablePublishButton();
+ this.hintOrWarning.setTextColor(getWarningTextColor());
+ this.hintOrWarning
+ .setText(R.string.error_publish_avatar_no_server_support);
+ }
+ if (this.defaultUri != null && uri.equals(this.defaultUri)) {
+ this.secondaryHint.setVisibility(View.INVISIBLE);
+ this.avatar.setOnLongClickListener(null);
+ } else if (this.defaultUri != null) {
+ this.secondaryHint.setVisibility(View.VISIBLE);
+ this.avatar.setOnLongClickListener(this.backToDefaultListener);
+ }
+ }
+
+ protected void enablePublishButton() {
+ this.publishButton.setEnabled(true);
+ this.publishButton.setTextColor(getPrimaryTextColor());
+ }
+
+ protected void disablePublishButton() {
+ this.publishButton.setEnabled(false);
+ this.publishButton.setTextColor(getSecondaryTextColor());
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
new file mode 100644
index 00000000..fc6308fc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -0,0 +1,74 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+import eu.siacs.conversations.entities.Account;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.PreferenceManager;
+
+public class SettingsActivity extends XmppActivity implements
+ OnSharedPreferenceChangeListener {
+ private SettingsFragment mSettingsFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSettingsFragment = new SettingsFragment();
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, mSettingsFragment).commit();
+ }
+
+ @Override
+ void onBackendConnected() {
+
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .registerOnSharedPreferenceChangeListener(this);
+ ListPreference resources = (ListPreference) mSettingsFragment
+ .findPreference("resource");
+ if (resources != null) {
+ ArrayList<CharSequence> entries = new ArrayList<CharSequence>(
+ Arrays.asList(resources.getEntries()));
+ entries.add(0, Build.MODEL);
+ resources.setEntries(entries.toArray(new CharSequence[entries
+ .size()]));
+ resources.setEntryValues(entries.toArray(new CharSequence[entries
+ .size()]));
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences preferences,
+ String name) {
+ if (name.equals("resource")) {
+ String resource = preferences.getString("resource", "mobile")
+ .toLowerCase(Locale.US);
+ if (xmppConnectionServiceBound) {
+ for (Account account : xmppConnectionService.getAccounts()) {
+ account.setResource(resource);
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ xmppConnectionService.reconnectAccount(account, false);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
new file mode 100644
index 00000000..7e1c3698
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java
@@ -0,0 +1,15 @@
+package eu.siacs.conversations.ui;
+
+import eu.siacs.conversations.R;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+public class SettingsFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
new file mode 100644
index 00000000..9fbc3db1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
@@ -0,0 +1,185 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.adapter.ConversationAdapter;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.Toast;
+
+public class ShareWithActivity extends XmppActivity {
+
+ private class Share {
+ public Uri uri;
+ public String account;
+ public String contact;
+ public String text;
+ }
+
+ private Share share;
+
+ private static final int REQUEST_START_NEW_CONVERSATION = 0x0501;
+ private ListView mListView;
+ private List<Conversation> mConversations = new ArrayList<Conversation>();
+
+ private UiCallback<Message> attachImageCallback = new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void success(Message message) {
+ xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int errorCode, Message object) {
+ // TODO Auto-generated method stub
+
+ }
+ };
+
+ protected void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_START_NEW_CONVERSATION
+ && resultCode == RESULT_OK) {
+ share.contact = data.getStringExtra("contact");
+ share.account = data.getStringExtra("account");
+ Log.d(Config.LOGTAG, "contact: " + share.contact + " account:"
+ + share.account);
+ }
+ if (xmppConnectionServiceBound && share != null
+ && share.contact != null && share.account != null) {
+ share();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setHomeButtonEnabled(false);
+
+ setContentView(R.layout.share_with);
+ setTitle(getString(R.string.title_activity_sharewith));
+
+ mListView = (ListView) findViewById(R.id.choose_conversation_list);
+ ConversationAdapter mAdapter = new ConversationAdapter(this,
+ this.mConversations);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1,
+ int position, long arg3) {
+ Conversation conversation = mConversations.get(position);
+ if (conversation.getMode() == Conversation.MODE_SINGLE
+ || share.uri == null) {
+ share(mConversations.get(position));
+ }
+ }
+ });
+
+ this.share = new Share();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.share_with, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add:
+ Intent intent = new Intent(getApplicationContext(),
+ ChooseContactActivity.class);
+ startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onStart() {
+ if (getIntent().getType() != null
+ && getIntent().getType().startsWith("image/")) {
+ this.share.uri = (Uri) getIntent().getParcelableExtra(
+ Intent.EXTRA_STREAM);
+ } else {
+ this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+ }
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.populateWithOrderedConversations(
+ mConversations, this.share.uri == null);
+ }
+ super.onStart();
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (xmppConnectionServiceBound && share != null
+ && share.contact != null && share.account != null) {
+ share();
+ return;
+ }
+ xmppConnectionService.populateWithOrderedConversations(mConversations,
+ this.share != null && this.share.uri == null);
+ }
+
+ private void share() {
+ Account account = xmppConnectionService.findAccountByJid(share.account);
+ if (account == null) {
+ return;
+ }
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account, share.contact, false);
+ share(conversation);
+ }
+
+ private void share(final Conversation conversation) {
+ if (share.uri != null) {
+ selectPresence(conversation, new OnPresenceSelected() {
+ @Override
+ public void onPresenceSelected() {
+ Toast.makeText(getApplicationContext(),
+ getText(R.string.preparing_image),
+ Toast.LENGTH_LONG).show();
+ ShareWithActivity.this.xmppConnectionService
+ .attachImageToConversation(conversation, share.uri,
+ attachImageCallback);
+ switchToConversation(conversation, null, true);
+ finish();
+ }
+ });
+
+ } else {
+ switchToConversation(conversation, this.share.text, true);
+ finish();
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
new file mode 100644
index 00000000..a1a2d4c2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -0,0 +1,677 @@
+package eu.siacs.conversations.ui;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.ActionBar.TabListener;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.ui.adapter.ListItemAdapter;
+import eu.siacs.conversations.utils.Validator;
+
+public class StartConversationActivity extends XmppActivity {
+
+ private Tab mContactsTab;
+ private Tab mConferencesTab;
+ private ViewPager mViewPager;
+
+ private MyListFragment mContactsListFragment = new MyListFragment();
+ private List<ListItem> contacts = new ArrayList<ListItem>();
+ private ArrayAdapter<ListItem> mContactsAdapter;
+
+ private MyListFragment mConferenceListFragment = new MyListFragment();
+ private List<ListItem> conferences = new ArrayList<ListItem>();
+ private ArrayAdapter<ListItem> mConferenceAdapter;
+
+ private List<String> mActivatedAccounts = new ArrayList<String>();
+ private List<String> mKnownHosts;
+ private List<String> mKnownConferenceHosts;
+
+ private Menu mOptionsMenu;
+ private EditText mSearchEditText;
+
+ public int conference_context_id;
+ public int contact_context_id;
+
+ private TabListener mTabListener = new TabListener() {
+
+ @Override
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ return;
+ }
+
+ @Override
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ mViewPager.setCurrentItem(tab.getPosition());
+ onTabChanged();
+ }
+
+ @Override
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ return;
+ }
+ };
+
+ private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ getActionBar().setSelectedNavigationItem(position);
+ onTabChanged();
+ }
+ };
+
+ private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ mSearchEditText.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mSearchEditText.requestFocus();
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mSearchEditText,
+ InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ mSearchEditText.setText("");
+ filter(null);
+ return true;
+ }
+ };
+ private TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ filter(editable.toString());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ }
+ };
+ private OnRosterUpdate onRosterUpdate = new OnRosterUpdate() {
+
+ @Override
+ public void onRosterUpdate() {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (mSearchEditText != null) {
+ filter(mSearchEditText.getText().toString());
+ }
+ }
+ });
+ }
+ };
+ private MenuItem mMenuSearchView;
+ private String mInitialJid;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_start_conversation);
+ mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager);
+ ActionBar actionBar = getActionBar();
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+ mContactsTab = actionBar.newTab().setText(R.string.contacts)
+ .setTabListener(mTabListener);
+ mConferencesTab = actionBar.newTab().setText(R.string.conferences)
+ .setTabListener(mTabListener);
+ actionBar.addTab(mContactsTab);
+ actionBar.addTab(mConferencesTab);
+
+ mViewPager.setOnPageChangeListener(mOnPageChangeListener);
+ mViewPager.setAdapter(new FragmentPagerAdapter(getFragmentManager()) {
+
+ @Override
+ public int getCount() {
+ return 2;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ if (position == 0) {
+ return mContactsListFragment;
+ } else {
+ return mConferenceListFragment;
+ }
+ }
+ });
+
+ mConferenceAdapter = new ListItemAdapter(this, conferences);
+ mConferenceListFragment.setListAdapter(mConferenceAdapter);
+ mConferenceListFragment.setContextMenu(R.menu.conference_context);
+ mConferenceListFragment
+ .setOnListItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1,
+ int position, long arg3) {
+ openConversationForBookmark(position);
+ }
+ });
+
+ mContactsAdapter = new ListItemAdapter(this, contacts);
+ mContactsListFragment.setListAdapter(mContactsAdapter);
+ mContactsListFragment.setContextMenu(R.menu.contact_context);
+ mContactsListFragment
+ .setOnListItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View arg1,
+ int position, long arg3) {
+ openConversationForContact(position);
+ }
+ });
+
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ xmppConnectionService.removeOnRosterUpdateListener();
+ }
+
+ protected void openConversationForContact(int position) {
+ Contact contact = (Contact) contacts.get(position);
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(contact.getAccount(),
+ contact.getJid(), false);
+ switchToConversation(conversation);
+ }
+
+ protected void openConversationForContact() {
+ int position = contact_context_id;
+ openConversationForContact(position);
+ }
+
+ protected void openConversationForBookmark() {
+ openConversationForBookmark(conference_context_id);
+ }
+
+ protected void openConversationForBookmark(int position) {
+ Bookmark bookmark = (Bookmark) conferences.get(position);
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(bookmark.getAccount(),
+ bookmark.getJid(), true);
+ conversation.setBookmark(bookmark);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService.joinMuc(conversation);
+ }
+ if (!bookmark.autojoin()) {
+ bookmark.setAutojoin(true);
+ xmppConnectionService.pushBookmarks(bookmark.getAccount());
+ }
+ switchToConversation(conversation);
+ }
+
+ protected void openDetailsForContact() {
+ int position = contact_context_id;
+ Contact contact = (Contact) contacts.get(position);
+ switchToContactDetails(contact);
+ }
+
+ protected void deleteContact() {
+ int position = contact_context_id;
+ final Contact contact = (Contact) contacts.get(position);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setTitle(R.string.action_delete_contact);
+ builder.setMessage(getString(R.string.remove_contact_text,
+ contact.getJid()));
+ builder.setPositiveButton(R.string.delete, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ xmppConnectionService.deleteContactOnServer(contact);
+ filter(mSearchEditText.getText().toString());
+ }
+ });
+ builder.create().show();
+
+ }
+
+ protected void deleteConference() {
+ int position = conference_context_id;
+ final Bookmark bookmark = (Bookmark) conferences.get(position);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setTitle(R.string.delete_bookmark);
+ builder.setMessage(getString(R.string.remove_bookmark_text,
+ bookmark.getJid()));
+ builder.setPositiveButton(R.string.delete, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ bookmark.unregisterConversation();
+ Account account = bookmark.getAccount();
+ account.getBookmarks().remove(bookmark);
+ xmppConnectionService.pushBookmarks(account);
+ filter(mSearchEditText.getText().toString());
+ }
+ });
+ builder.create().show();
+
+ }
+
+ @SuppressLint("InflateParams")
+ protected void showCreateContactDialog(String prefilledJid) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.create_contact);
+ View dialogView = getLayoutInflater().inflate(
+ R.layout.create_contact_dialog, null);
+ final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
+ final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView
+ .findViewById(R.id.jid);
+ jid.setAdapter(new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1, mKnownHosts));
+ if (prefilledJid != null) {
+ jid.append(prefilledJid);
+ }
+ populateAccountSpinner(spinner);
+ builder.setView(dialogView);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.create, null);
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
+ new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (!xmppConnectionServiceBound) {
+ return;
+ }
+ if (Validator.isValidJid(jid.getText().toString())) {
+ String accountJid = (String) spinner
+ .getSelectedItem();
+ String contactJid = jid.getText().toString();
+ Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ dialog.dismiss();
+ return;
+ }
+ Contact contact = account.getRoster().getContact(
+ contactJid);
+ if (contact.showInRoster()) {
+ jid.setError(getString(R.string.contact_already_exists));
+ } else {
+ xmppConnectionService.createContact(contact);
+ dialog.dismiss();
+ switchToConversation(contact);
+ }
+ } else {
+ jid.setError(getString(R.string.invalid_jid));
+ }
+ }
+ });
+
+ }
+
+ @SuppressLint("InflateParams")
+ protected void showJoinConferenceDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.join_conference);
+ View dialogView = getLayoutInflater().inflate(
+ R.layout.join_conference_dialog, null);
+ final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
+ final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView
+ .findViewById(R.id.jid);
+ jid.setAdapter(new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1, mKnownConferenceHosts));
+ populateAccountSpinner(spinner);
+ final CheckBox bookmarkCheckBox = (CheckBox) dialogView
+ .findViewById(R.id.bookmark);
+ builder.setView(dialogView);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.join, null);
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
+ new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (!xmppConnectionServiceBound) {
+ return;
+ }
+ if (Validator.isValidJid(jid.getText().toString())) {
+ String accountJid = (String) spinner
+ .getSelectedItem();
+ String conferenceJid = jid.getText().toString();
+ Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ dialog.dismiss();
+ return;
+ }
+ if (bookmarkCheckBox.isChecked()) {
+ if (account.hasBookmarkFor(conferenceJid)) {
+ jid.setError(getString(R.string.bookmark_already_exists));
+ } else {
+ Bookmark bookmark = new Bookmark(account,
+ conferenceJid);
+ bookmark.setAutojoin(true);
+ account.getBookmarks().add(bookmark);
+ xmppConnectionService
+ .pushBookmarks(account);
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account,
+ conferenceJid, true);
+ conversation.setBookmark(bookmark);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService
+ .joinMuc(conversation);
+ }
+ dialog.dismiss();
+ switchToConversation(conversation);
+ }
+ } else {
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account,
+ conferenceJid, true);
+ if (!conversation.getMucOptions().online()) {
+ xmppConnectionService.joinMuc(conversation);
+ }
+ dialog.dismiss();
+ switchToConversation(conversation);
+ }
+ } else {
+ jid.setError(getString(R.string.invalid_jid));
+ }
+ }
+ });
+ }
+
+ protected void switchToConversation(Contact contact) {
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(contact.getAccount(),
+ contact.getJid(), false);
+ switchToConversation(conversation);
+ }
+
+ private void populateAccountSpinner(Spinner spinner) {
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, mActivatedAccounts);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ this.mOptionsMenu = menu;
+ getMenuInflater().inflate(R.menu.start_conversation, menu);
+ MenuItem menuCreateContact = (MenuItem) menu
+ .findItem(R.id.action_create_contact);
+ MenuItem menuCreateConference = (MenuItem) menu
+ .findItem(R.id.action_join_conference);
+ mMenuSearchView = (MenuItem) menu.findItem(R.id.action_search);
+ mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+ View mSearchView = mMenuSearchView.getActionView();
+ mSearchEditText = (EditText) mSearchView
+ .findViewById(R.id.search_field);
+ mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+ if (getActionBar().getSelectedNavigationIndex() == 0) {
+ menuCreateConference.setVisible(false);
+ } else {
+ menuCreateContact.setVisible(false);
+ }
+ if (mInitialJid != null) {
+ mMenuSearchView.expandActionView();
+ mSearchEditText.append(mInitialJid);
+ filter(mInitialJid);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_create_contact:
+ showCreateContactDialog(null);
+ break;
+ case R.id.action_join_conference:
+ showJoinConferenceDialog();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
+ mOptionsMenu.findItem(R.id.action_search).expandActionView();
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ protected void onBackendConnected() {
+ xmppConnectionService.setOnRosterUpdateListener(this.onRosterUpdate);
+ this.mActivatedAccounts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.STATUS_DISABLED) {
+ this.mActivatedAccounts.add(account.getJid());
+ }
+ }
+ this.mKnownHosts = xmppConnectionService.getKnownHosts();
+ this.mKnownConferenceHosts = xmppConnectionService
+ .getKnownConferenceHosts();
+ if (!startByIntent()) {
+ if (mSearchEditText != null) {
+ filter(mSearchEditText.getText().toString());
+ } else {
+ filter(null);
+ }
+ }
+ }
+
+ protected boolean startByIntent() {
+ if (getIntent() != null
+ && Intent.ACTION_SENDTO.equals(getIntent().getAction())) {
+ try {
+ String jid = URLDecoder.decode(
+ getIntent().getData().getEncodedPath(), "UTF-8").split(
+ "/")[1];
+ setIntent(null);
+ return handleJid(jid);
+ } catch (UnsupportedEncodingException e) {
+ setIntent(null);
+ return false;
+ }
+ } else if (getIntent() != null
+ && Intent.ACTION_VIEW.equals(getIntent().getAction())) {
+ Uri uri = getIntent().getData();
+ String jid = uri.getSchemeSpecificPart().split("\\?")[0];
+ return handleJid(jid);
+ }
+ return false;
+ }
+
+ private boolean handleJid(String jid) {
+ List<Contact> contacts = xmppConnectionService.findContacts(jid);
+ if (contacts.size() == 0) {
+ showCreateContactDialog(jid);
+ return false;
+ } else if (contacts.size() == 1) {
+ switchToConversation(contacts.get(0));
+ return true;
+ } else {
+ if (mMenuSearchView != null) {
+ mMenuSearchView.expandActionView();
+ mSearchEditText.setText(jid);
+ filter(jid);
+ } else {
+ mInitialJid = jid;
+ }
+ return true;
+ }
+ }
+
+ protected void filter(String needle) {
+ if (xmppConnectionServiceBound) {
+ this.filterContacts(needle);
+ this.filterConferences(needle);
+ }
+ }
+
+ protected void filterContacts(String needle) {
+ this.contacts.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.STATUS_DISABLED) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster() && contact.match(needle)) {
+ this.contacts.add(contact);
+ }
+ }
+ }
+ }
+ Collections.sort(this.contacts);
+ mContactsAdapter.notifyDataSetChanged();
+ }
+
+ protected void filterConferences(String needle) {
+ this.conferences.clear();
+ for (Account account : xmppConnectionService.getAccounts()) {
+ if (account.getStatus() != Account.STATUS_DISABLED) {
+ for (Bookmark bookmark : account.getBookmarks()) {
+ if (bookmark.match(needle)) {
+ this.conferences.add(bookmark);
+ }
+ }
+ }
+ }
+ Collections.sort(this.conferences);
+ mConferenceAdapter.notifyDataSetChanged();
+ }
+
+ private void onTabChanged() {
+ invalidateOptionsMenu();
+ }
+
+ public static class MyListFragment extends ListFragment {
+ private AdapterView.OnItemClickListener mOnItemClickListener;
+ private int mResContextMenu;
+
+ public void setContextMenu(int res) {
+ this.mResContextMenu = res;
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ if (mOnItemClickListener != null) {
+ mOnItemClickListener.onItemClick(l, v, position, id);
+ }
+ }
+
+ public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
+ this.mOnItemClickListener = l;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ registerForContextMenu(getListView());
+ getListView().setFastScrollEnabled(true);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ StartConversationActivity activity = (StartConversationActivity) getActivity();
+ activity.getMenuInflater().inflate(mResContextMenu, menu);
+ AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ if (mResContextMenu == R.menu.conference_context) {
+ activity.conference_context_id = acmi.position;
+ } else {
+ activity.contact_context_id = acmi.position;
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ StartConversationActivity activity = (StartConversationActivity) getActivity();
+ switch (item.getItemId()) {
+ case R.id.context_start_conversation:
+ activity.openConversationForContact();
+ break;
+ case R.id.context_contact_details:
+ activity.openDetailsForContact();
+ break;
+ case R.id.context_delete_contact:
+ activity.deleteContact();
+ break;
+ case R.id.context_join_conference:
+ activity.openConversationForBookmark();
+ break;
+ case R.id.context_delete_conference:
+ activity.deleteConference();
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java
new file mode 100644
index 00000000..c80199e1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/UiCallback.java
@@ -0,0 +1,11 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+
+public interface UiCallback<T> {
+ 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/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
new file mode 100644
index 00000000..d26f0e31
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -0,0 +1,637 @@
+package eu.siacs.conversations.ui;
+
+import java.io.FileNotFoundException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.app.AlertDialog.Builder;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnClickListener;
+import android.content.IntentSender.SendIntentException;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.text.InputType;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+public abstract class XmppActivity extends Activity {
+
+ protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
+ protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
+
+ public XmppConnectionService xmppConnectionService;
+ public boolean xmppConnectionServiceBound = false;
+ protected boolean handledViewIntent = false;
+
+ protected int mPrimaryTextColor;
+ protected int mSecondaryTextColor;
+ protected int mSecondaryBackgroundColor;
+ protected int mColorRed;
+ protected int mColorOrange;
+ protected int mColorGreen;
+ protected int mPrimaryColor;
+
+ protected boolean mUseSubject = true;
+
+ private DisplayMetrics metrics;
+
+ protected interface OnValueEdited {
+ public void onValueEdited(String value);
+ }
+
+ public interface OnPresenceSelected {
+ public void onPresenceSelected();
+ }
+
+ protected ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ XmppConnectionBinder binder = (XmppConnectionBinder) service;
+ xmppConnectionService = binder.getService();
+ xmppConnectionServiceBound = true;
+ onBackendConnected();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ xmppConnectionServiceBound = false;
+ }
+ };
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (!xmppConnectionServiceBound) {
+ connectToBackend();
+ }
+ }
+
+ public void connectToBackend() {
+ Intent intent = new Intent(this, XmppConnectionService.class);
+ intent.setAction("ui");
+ startService(intent);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (xmppConnectionServiceBound) {
+ unbindService(mConnection);
+ xmppConnectionServiceBound = false;
+ }
+ }
+
+ protected void hideKeyboard() {
+ InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ View focus = getCurrentFocus();
+
+ if (focus != null) {
+
+ inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
+ InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+ public boolean hasPgp() {
+ return xmppConnectionService.getPgpEngine() != null;
+ }
+
+ public void showInstallPgpDialog() {
+ Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.openkeychain_required));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getText(R.string.openkeychain_required_long));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setNeutralButton(getString(R.string.restart),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (xmppConnectionServiceBound) {
+ unbindService(mConnection);
+ xmppConnectionServiceBound = false;
+ }
+ stopService(new Intent(XmppActivity.this,
+ XmppConnectionService.class));
+ finish();
+ }
+ });
+ builder.setPositiveButton(getString(R.string.install),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Uri uri = Uri
+ .parse("market://details?id=org.sufficientlysecure.keychain");
+ Intent marketIntent = new Intent(Intent.ACTION_VIEW,
+ uri);
+ PackageManager manager = getApplicationContext()
+ .getPackageManager();
+ List<ResolveInfo> infos = manager
+ .queryIntentActivities(marketIntent, 0);
+ if (infos.size() > 0) {
+ startActivity(marketIntent);
+ } else {
+ uri = Uri.parse("http://www.openkeychain.org/");
+ Intent browserIntent = new Intent(
+ Intent.ACTION_VIEW, uri);
+ startActivity(browserIntent);
+ }
+ finish();
+ }
+ });
+ builder.create().show();
+ }
+
+ abstract void onBackendConnected();
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ break;
+ case R.id.action_accounts:
+ startActivity(new Intent(this, ManageAccountActivity.class));
+ break;
+ case android.R.id.home:
+ finish();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ metrics = getResources().getDisplayMetrics();
+ ExceptionHelper.init(getApplicationContext());
+ mPrimaryTextColor = getResources().getColor(R.color.primarytext);
+ mSecondaryTextColor = getResources().getColor(R.color.secondarytext);
+ mColorRed = getResources().getColor(R.color.red);
+ mColorOrange = getResources().getColor(R.color.orange);
+ mColorGreen = getResources().getColor(R.color.green);
+ mPrimaryColor = getResources().getColor(R.color.primary);
+ mSecondaryBackgroundColor = getResources().getColor(
+ R.color.secondarybackground);
+ if (getPreferences().getBoolean("use_larger_font", false)) {
+ setTheme(R.style.ConversationsTheme_LargerText);
+ }
+ mUseSubject = getPreferences().getBoolean("use_subject", true);
+ }
+
+ protected SharedPreferences getPreferences() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+ }
+
+ public boolean useSubjectToIdentifyConference() {
+ return mUseSubject;
+ }
+
+ public void switchToConversation(Conversation conversation) {
+ switchToConversation(conversation, null, false);
+ }
+
+ public void switchToConversation(Conversation conversation, String text,
+ boolean newTask) {
+ Intent viewConversationIntent = new Intent(this,
+ ConversationActivity.class);
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+ conversation.getUuid());
+ if (text != null) {
+ viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
+ }
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+ if (newTask) {
+ viewConversationIntent.setFlags(viewConversationIntent.getFlags()
+ | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ } else {
+ viewConversationIntent.setFlags(viewConversationIntent.getFlags()
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ }
+ startActivity(viewConversationIntent);
+ finish();
+ }
+
+ public void switchToContactDetails(Contact contact) {
+ Intent intent = new Intent(this, ContactDetailsActivity.class);
+ intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
+ intent.putExtra("account", contact.getAccount().getJid());
+ intent.putExtra("contact", contact.getJid());
+ startActivity(intent);
+ }
+
+ public void switchToAccount(Account account) {
+ Intent intent = new Intent(this, EditAccountActivity.class);
+ intent.putExtra("jid", account.getJid());
+ startActivity(intent);
+ }
+
+ protected void inviteToConversation(Conversation conversation) {
+ Intent intent = new Intent(getApplicationContext(),
+ ChooseContactActivity.class);
+ intent.putExtra("conversation", conversation.getUuid());
+ startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
+ }
+
+ protected void announcePgp(Account account, final Conversation conversation) {
+ xmppConnectionService.getPgpEngine().generateSignature(account,
+ "online", new UiCallback<Account>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi,
+ Account account) {
+ try {
+ startIntentSenderForResult(pi.getIntentSender(),
+ REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+ } catch (SendIntentException e) {
+ }
+ }
+
+ @Override
+ public void success(Account account) {
+ xmppConnectionService.databaseBackend
+ .updateAccount(account);
+ xmppConnectionService.sendPresencePacket(account,
+ xmppConnectionService.getPresenceGenerator()
+ .sendPresence(account));
+ if (conversation != null) {
+ conversation
+ .setNextEncryption(Message.ENCRYPTION_PGP);
+ xmppConnectionService.databaseBackend
+ .updateConversation(conversation);
+ }
+ }
+
+ @Override
+ public void error(int error, Account account) {
+ displayErrorDialog(error);
+ }
+ });
+ }
+
+ protected void displayErrorDialog(final int errorCode) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ XmppActivity.this);
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setTitle(getString(R.string.error));
+ builder.setMessage(errorCode);
+ builder.setNeutralButton(R.string.accept, null);
+ builder.create().show();
+ }
+ });
+
+ }
+
+ protected void showAddToRosterDialog(final Conversation conversation) {
+ String jid = conversation.getContactJid();
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(jid);
+ builder.setMessage(getString(R.string.not_in_roster));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.add_contact),
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String jid = conversation.getContactJid();
+ Account account = conversation.getAccount();
+ Contact contact = account.getRoster().getContact(jid);
+ xmppConnectionService.createContact(contact);
+ switchToContactDetails(contact);
+ }
+ });
+ builder.create().show();
+ }
+
+ private void showAskForPresenceDialog(final Contact contact) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(contact.getJid());
+ builder.setMessage(R.string.request_presence_updates);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.request_now,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.sendPresencePacket(contact
+ .getAccount(), xmppConnectionService
+ .getPresenceGenerator()
+ .requestPresenceUpdatesFrom(contact));
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ private void warnMutalPresenceSubscription(final Conversation conversation,
+ final OnPresenceSelected listener) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(conversation.getContact().getJid());
+ builder.setMessage(R.string.without_mutual_presence_updates);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ignore, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ conversation.setNextPresence(null);
+ if (listener != null) {
+ listener.onPresenceSelected();
+ }
+ }
+ });
+ builder.create().show();
+ }
+
+ protected void quickEdit(String previousValue, OnValueEdited callback) {
+ quickEdit(previousValue, callback, false);
+ }
+
+ protected void quickPasswordEdit(String previousValue,
+ OnValueEdited callback) {
+ quickEdit(previousValue, callback, true);
+ }
+
+ @SuppressLint("InflateParams")
+ private void quickEdit(final String previousValue,
+ final OnValueEdited callback, boolean password) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ View view = (View) getLayoutInflater()
+ .inflate(R.layout.quickedit, null);
+ final EditText editor = (EditText) view.findViewById(R.id.editor);
+ OnClickListener mClickListener = new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String value = editor.getText().toString();
+ if (!previousValue.equals(value) && value.trim().length() > 0) {
+ callback.onValueEdited(value);
+ }
+ }
+ };
+ if (password) {
+ editor.setInputType(InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ editor.setHint(R.string.password);
+ builder.setPositiveButton(R.string.accept, mClickListener);
+ } else {
+ builder.setPositiveButton(R.string.edit, mClickListener);
+ }
+ editor.requestFocus();
+ editor.setText(previousValue);
+ builder.setView(view);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.create().show();
+ }
+
+ public void selectPresence(final Conversation conversation,
+ final OnPresenceSelected listener) {
+ Contact contact = conversation.getContact();
+ if (!contact.showInRoster()) {
+ showAddToRosterDialog(conversation);
+ } else {
+ Presences presences = contact.getPresences();
+ if (presences.size() == 0) {
+ if (!contact.getOption(Contact.Options.TO)
+ && !contact.getOption(Contact.Options.ASKING)
+ && contact.getAccount().getStatus() == Account.STATUS_ONLINE) {
+ showAskForPresenceDialog(contact);
+ } else if (!contact.getOption(Contact.Options.TO)
+ || !contact.getOption(Contact.Options.FROM)) {
+ warnMutalPresenceSubscription(conversation, listener);
+ } else {
+ conversation.setNextPresence(null);
+ listener.onPresenceSelected();
+ }
+ } else if (presences.size() == 1) {
+ String presence = (String) presences.asStringArray()[0];
+ conversation.setNextPresence(presence);
+ listener.onPresenceSelected();
+ } else {
+ final StringBuilder presence = new StringBuilder();
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.choose_presence));
+ final String[] presencesArray = presences.asStringArray();
+ int preselectedPresence = 0;
+ for (int i = 0; i < presencesArray.length; ++i) {
+ if (presencesArray[i].equals(contact.lastseen.presence)) {
+ preselectedPresence = i;
+ break;
+ }
+ }
+ presence.append(presencesArray[preselectedPresence]);
+ builder.setSingleChoiceItems(presencesArray,
+ preselectedPresence,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ presence.delete(0, presence.length());
+ presence.append(presencesArray[which]);
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ conversation.setNextPresence(presence.toString());
+ listener.onPresenceSelected();
+ }
+ });
+ builder.create().show();
+ }
+ }
+ }
+
+ protected void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_INVITE_TO_CONVERSATION
+ && resultCode == RESULT_OK) {
+ String contactJid = data.getStringExtra("contact");
+ String conversationUuid = data.getStringExtra("conversation");
+ Conversation conversation = xmppConnectionService
+ .findConversationByUuid(conversationUuid);
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ xmppConnectionService.invite(conversation, contactJid);
+ }
+ Log.d(Config.LOGTAG, "inviting " + contactJid + " to "
+ + conversation.getName());
+ }
+ }
+
+ public int getSecondaryTextColor() {
+ return this.mSecondaryTextColor;
+ }
+
+ public int getPrimaryTextColor() {
+ return this.mPrimaryTextColor;
+ }
+
+ public int getWarningTextColor() {
+ return this.mColorRed;
+ }
+
+ public int getPrimaryColor() {
+ return this.mPrimaryColor;
+ }
+
+ public int getSecondaryBackgroundColor() {
+ return this.mSecondaryBackgroundColor;
+ }
+
+ public int getPixel(int dp) {
+ DisplayMetrics metrics = getResources().getDisplayMetrics();
+ return ((int) (dp * metrics.density));
+ }
+
+ public AvatarService avatarService() {
+ return xmppConnectionService.getAvatarService();
+ }
+
+ class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private Message message = null;
+
+ public BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Message... params) {
+ message = params[0];
+ try {
+ return xmppConnectionService.getFileBackend().getThumbnail(
+ message, (int) (metrics.density * 288), false);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (imageViewReference != null && bitmap != null) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ public void loadBitmap(Message message, ImageView imageView) {
+ Bitmap bm;
+ try {
+ bm = xmppConnectionService.getFileBackend().getThumbnail(message,
+ (int) (metrics.density * 288), true);
+ } catch (FileNotFoundException e) {
+ bm = null;
+ }
+ if (bm != null) {
+ imageView.setImageBitmap(bm);
+ imageView.setBackgroundColor(0x00000000);
+ } else {
+ if (cancelPotentialWork(message, imageView)) {
+ imageView.setBackgroundColor(0xff333333);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(
+ getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(message);
+ } catch (RejectedExecutionException e) {
+ return;
+ }
+ }
+ }
+ }
+
+ public static boolean cancelPotentialWork(Message message,
+ ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Message oldMessage = bitmapWorkerTask.message;
+ if (oldMessage == null || message != oldMessage) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+ public AsyncDrawable(Resources res, Bitmap bitmap,
+ BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
+ 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
new file mode 100644
index 00000000..4ca21a3b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
@@ -0,0 +1,102 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.XmppActivity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class AccountAdapter extends ArrayAdapter<Account> {
+
+ private XmppActivity activity;
+
+ public AccountAdapter(XmppActivity activity, List<Account> objects) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ Account account = getItem(position);
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = (View) inflater.inflate(R.layout.account_row, parent, false);
+ }
+ TextView jid = (TextView) view.findViewById(R.id.account_jid);
+ jid.setText(account.getJid());
+ TextView statusView = (TextView) view.findViewById(R.id.account_status);
+ ImageView imageView = (ImageView) view.findViewById(R.id.account_image);
+ imageView.setImageBitmap(activity.avatarService().get(account,
+ activity.getPixel(48)));
+ switch (account.getStatus()) {
+ case Account.STATUS_DISABLED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_disabled));
+ statusView.setTextColor(activity.getSecondaryTextColor());
+ break;
+ case Account.STATUS_ONLINE:
+ statusView.setText(getContext().getString(
+ R.string.account_status_online));
+ statusView.setTextColor(activity.getPrimaryColor());
+ break;
+ case Account.STATUS_CONNECTING:
+ statusView.setText(getContext().getString(
+ R.string.account_status_connecting));
+ statusView.setTextColor(activity.getSecondaryTextColor());
+ break;
+ case Account.STATUS_OFFLINE:
+ statusView.setText(getContext().getString(
+ R.string.account_status_offline));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_UNAUTHORIZED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_unauthorized));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_SERVER_NOT_FOUND:
+ statusView.setText(getContext().getString(
+ R.string.account_status_not_found));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_NO_INTERNET:
+ statusView.setText(getContext().getString(
+ R.string.account_status_no_internet));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_FAILED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_fail));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_CONFLICT:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_conflict));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_SUCCESSFULL:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_success));
+ statusView.setTextColor(activity.getSecondaryTextColor());
+ break;
+ case Account.STATUS_REGISTRATION_NOT_SUPPORTED:
+ statusView.setText(getContext().getString(
+ R.string.account_status_regis_not_sup));
+ statusView.setTextColor(activity.getWarningTextColor());
+ break;
+ default:
+ statusView.setText("");
+ break;
+ }
+
+ return view;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
new file mode 100644
index 00000000..183c89fa
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -0,0 +1,135 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ConversationAdapter extends ArrayAdapter<Conversation> {
+
+ private XmppActivity activity;
+
+ public ConversationAdapter(XmppActivity activity,
+ List<Conversation> conversations) {
+ super(activity, 0, conversations);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) activity
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = (View) inflater.inflate(R.layout.conversation_list_row,
+ parent, false);
+ }
+ Conversation conversation = getItem(position);
+ if (this.activity instanceof ConversationActivity) {
+ ConversationActivity activity = (ConversationActivity) this.activity;
+ if (!activity.isConversationsOverviewHideable()) {
+ if (conversation == activity.getSelectedConversation()) {
+ view.setBackgroundColor(activity
+ .getSecondaryBackgroundColor());
+ } else {
+ view.setBackgroundColor(Color.TRANSPARENT);
+ }
+ } else {
+ view.setBackgroundColor(Color.TRANSPARENT);
+ }
+ }
+ TextView convName = (TextView) view
+ .findViewById(R.id.conversation_name);
+ if (conversation.getMode() == Conversation.MODE_SINGLE
+ || activity.useSubjectToIdentifyConference()) {
+ convName.setText(conversation.getName());
+ } else {
+ convName.setText(conversation.getContactJid().split("/")[0]);
+ }
+ TextView mLastMessage = (TextView) view
+ .findViewById(R.id.conversation_lastmsg);
+ TextView mTimestamp = (TextView) view
+ .findViewById(R.id.conversation_lastupdate);
+ ImageView imagePreview = (ImageView) view
+ .findViewById(R.id.conversation_lastimage);
+
+ Message message = conversation.getLatestMessage();
+
+ if (!conversation.isRead()) {
+ convName.setTypeface(null, Typeface.BOLD);
+ } else {
+ convName.setTypeface(null, Typeface.NORMAL);
+ }
+
+ if (message.getType() == Message.TYPE_IMAGE
+ || message.getDownloadable() != null) {
+ Downloadable d = message.getDownloadable();
+ if (d != null) {
+ mLastMessage.setVisibility(View.VISIBLE);
+ imagePreview.setVisibility(View.GONE);
+ if (conversation.isRead()) {
+ mLastMessage.setTypeface(null, Typeface.ITALIC);
+ } else {
+ mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC);
+ }
+ if (d.getStatus() == Downloadable.STATUS_CHECKING) {
+ mLastMessage.setText(R.string.checking_image);
+ } else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
+ mLastMessage.setText(R.string.receiving_image);
+ } else if (d.getStatus() == Downloadable.STATUS_OFFER) {
+ mLastMessage.setText(R.string.image_offered_for_download);
+ } else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
+ mLastMessage.setText(R.string.image_offered_for_download);
+ } else if (d.getStatus() == Downloadable.STATUS_DELETED) {
+ mLastMessage.setText(R.string.image_file_deleted);
+ } else {
+ mLastMessage.setText("");
+ }
+ } else {
+ mLastMessage.setVisibility(View.GONE);
+ imagePreview.setVisibility(View.VISIBLE);
+ activity.loadBitmap(message, imagePreview);
+ }
+ } else {
+ if ((message.getEncryption() != Message.ENCRYPTION_PGP)
+ && (message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
+ String body = Config.PARSE_EMOTICONS ? UIHelper
+ .transformAsciiEmoticons(message.getBody()) : message
+ .getBody();
+ mLastMessage.setText(body);
+ } else {
+ mLastMessage.setText(R.string.encrypted_message_received);
+ }
+ if (!conversation.isRead()) {
+ mLastMessage.setTypeface(null, Typeface.BOLD);
+ } else {
+ mLastMessage.setTypeface(null, Typeface.NORMAL);
+ }
+ mLastMessage.setVisibility(View.VISIBLE);
+ imagePreview.setVisibility(View.GONE);
+ }
+ mTimestamp.setText(UIHelper.readableTimeDifference(getContext(),
+ conversation.getLatestMessage().getTimeSent()));
+
+ ImageView profilePicture = (ImageView) view
+ .findViewById(R.id.conversation_image);
+ profilePicture.setImageBitmap(activity.avatarService().get(
+ conversation, activity.getPixel(56)));
+
+ return view;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
new file mode 100644
index 00000000..143dfda1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
@@ -0,0 +1,74 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+
+public class KnownHostsAdapter extends ArrayAdapter<String> {
+ private ArrayList<String> domains;
+ private Filter domainFilter = new Filter() {
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ if (constraint != null) {
+ ArrayList<String> suggestions = new ArrayList<String>();
+ 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<String> mKnownHosts) {
+ super(context, viewResourceId, mKnownHosts);
+ domains = new ArrayList<String>(mKnownHosts.size());
+ for (String domain : mKnownHosts) {
+ domains.add(new String(domain));
+ }
+ }
+
+ @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
new file mode 100644
index 00000000..977aa7b5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
@@ -0,0 +1,44 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.XmppActivity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ListItemAdapter extends ArrayAdapter<ListItem> {
+
+ protected XmppActivity activity;
+
+ public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ ListItem item = getItem(position);
+ if (view == null) {
+ view = (View) inflater.inflate(R.layout.contact, parent, false);
+ }
+ TextView name = (TextView) view.findViewById(R.id.contact_display_name);
+ TextView jid = (TextView) view.findViewById(R.id.contact_jid);
+ ImageView picture = (ImageView) view.findViewById(R.id.contact_photo);
+
+ jid.setText(item.getJid());
+ name.setText(item.getDisplayName());
+ picture.setImageBitmap(activity.avatarService().get(item,
+ activity.getPixel(48)));
+ return view;
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
new file mode 100644
index 00000000..a9a55cbf
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -0,0 +1,560 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Message.ImageParams;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class MessageAdapter extends ArrayAdapter<Message> {
+
+ private static final int SENT = 0;
+ private static final int RECEIVED = 1;
+ private static final int STATUS = 2;
+ private static final int NULL = 3;
+
+ private ConversationActivity activity;
+
+ private DisplayMetrics metrics;
+
+ private OnContactPictureClicked mOnContactPictureClickedListener;
+ private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
+
+ public MessageAdapter(ConversationActivity activity, List<Message> messages) {
+ super(activity, 0, messages);
+ this.activity = activity;
+ metrics = getContext().getResources().getDisplayMetrics();
+ }
+
+ public void setOnContactPictureClicked(OnContactPictureClicked listener) {
+ this.mOnContactPictureClickedListener = listener;
+ }
+
+ public void setOnContactPictureLongClicked(
+ OnContactPictureLongClicked listener) {
+ this.mOnContactPictureLongClickedListener = listener;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 4;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (getItem(position).wasMergedIntoPrevious()) {
+ return NULL;
+ } else if (getItem(position).getType() == Message.TYPE_STATUS) {
+ return STATUS;
+ } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) {
+ return RECEIVED;
+ } else {
+ return SENT;
+ }
+ }
+
+ private void displayStatus(ViewHolder viewHolder, Message message) {
+ String filesize = null;
+ String info = null;
+ boolean error = false;
+ if (viewHolder.indicatorReceived != null) {
+ viewHolder.indicatorReceived.setVisibility(View.GONE);
+ }
+ boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
+ && message.getMergedStatus() <= Message.STATUS_RECEIVED;
+ if (message.getType() == Message.TYPE_IMAGE
+ || message.getDownloadable() != null) {
+ ImageParams params = message.getImageParams();
+ if (params.size != 0) {
+ filesize = params.size / 1024 + " KB";
+ }
+ }
+ switch (message.getMergedStatus()) {
+ case Message.STATUS_WAITING:
+ info = getContext().getString(R.string.waiting);
+ break;
+ case Message.STATUS_UNSEND:
+ info = getContext().getString(R.string.sending);
+ break;
+ case Message.STATUS_OFFERED:
+ info = getContext().getString(R.string.offering);
+ break;
+ case Message.STATUS_SEND_RECEIVED:
+ if (activity.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_DISPLAYED:
+ if (activity.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_FAILED:
+ info = getContext().getString(R.string.send_failed);
+ error = true;
+ break;
+ case Message.STATUS_SEND_REJECTED:
+ info = getContext().getString(R.string.send_rejected);
+ error = true;
+ break;
+ default:
+ if (multiReceived) {
+ Contact contact = message.getContact();
+ if (contact != null) {
+ info = contact.getDisplayName();
+ } else {
+ if (message.getPresence() != null) {
+ info = message.getPresence();
+ } else {
+ info = message.getCounterpart();
+ }
+ }
+ }
+ break;
+ }
+ if (error) {
+ viewHolder.time.setTextColor(activity.getWarningTextColor());
+ } else {
+ viewHolder.time.setTextColor(activity.getSecondaryTextColor());
+ }
+ if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+ viewHolder.indicator.setVisibility(View.GONE);
+ } else {
+ viewHolder.indicator.setVisibility(View.VISIBLE);
+ }
+
+ String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
+ message.getMergedTimeSent());
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ if ((filesize != null) && (info != null)) {
+ viewHolder.time.setText(filesize + " \u00B7 " + info);
+ } else if ((filesize == null) && (info != null)) {
+ viewHolder.time.setText(formatedTime + " \u00B7 " + info);
+ } else if ((filesize != null) && (info == null)) {
+ viewHolder.time.setText(formatedTime + " \u00B7 " + filesize);
+ } else {
+ viewHolder.time.setText(formatedTime);
+ }
+ } else {
+ if ((filesize != null) && (info != null)) {
+ viewHolder.time.setText(filesize + " \u00B7 " + info);
+ } else if ((filesize == null) && (info != null)) {
+ if (error) {
+ viewHolder.time.setText(info + " \u00B7 " + formatedTime);
+ } else {
+ viewHolder.time.setText(info);
+ }
+ } else if ((filesize != null) && (info == null)) {
+ viewHolder.time.setText(filesize + " \u00B7 " + formatedTime);
+ } else {
+ viewHolder.time.setText(formatedTime);
+ }
+ }
+ }
+
+ private void displayInfoMessage(ViewHolder viewHolder, int r) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ viewHolder.messageBody.setText(getContext().getString(r));
+ viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
+ viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
+ viewHolder.messageBody.setTextIsSelectable(false);
+ }
+
+ private void displayDecryptionFailed(ViewHolder viewHolder) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ viewHolder.messageBody.setText(getContext().getString(
+ R.string.decryption_failed));
+ viewHolder.messageBody.setTextColor(activity.getWarningTextColor());
+ viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+ viewHolder.messageBody.setTextIsSelectable(false);
+ }
+
+ private void displayTextMessage(ViewHolder viewHolder, Message message) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ if (message.getBody() != null) {
+ if (message.getType() != Message.TYPE_PRIVATE) {
+ String body = Config.PARSE_EMOTICONS ? UIHelper
+ .transformAsciiEmoticons(message.getMergedBody())
+ : message.getMergedBody();
+ viewHolder.messageBody.setText(body);
+ } else {
+ String privateMarker;
+ if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ privateMarker = activity
+ .getString(R.string.private_message);
+ } else {
+ String to;
+ if (message.getPresence() != null) {
+ to = message.getPresence();
+ } else {
+ to = message.getCounterpart();
+ }
+ privateMarker = activity.getString(
+ R.string.private_message_to, to);
+ }
+ SpannableString span = new SpannableString(privateMarker + " "
+ + message.getBody());
+ span.setSpan(
+ new ForegroundColorSpan(activity
+ .getSecondaryTextColor()), 0, privateMarker
+ .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ span.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0,
+ privateMarker.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ viewHolder.messageBody.setText(span);
+ }
+ } else {
+ viewHolder.messageBody.setText("");
+ }
+ viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor());
+ viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+ viewHolder.messageBody.setTextIsSelectable(true);
+ }
+
+ private void displayDownloadableMessage(ViewHolder viewHolder,
+ final Message message, int resid) {
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.messageBody.setVisibility(View.GONE);
+ viewHolder.download_button.setVisibility(View.VISIBLE);
+ viewHolder.download_button.setText(resid);
+ viewHolder.download_button.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ startDonwloadable(message);
+ }
+ });
+ }
+
+ private void displayImageMessage(ViewHolder viewHolder,
+ final Message message) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.messageBody.setVisibility(View.GONE);
+ viewHolder.image.setVisibility(View.VISIBLE);
+ ImageParams params = message.getImageParams();
+ double target = metrics.density * 288;
+ int scalledW;
+ int scalledH;
+ if (params.width <= params.height) {
+ scalledW = (int) (params.width / ((double) params.height / target));
+ scalledH = (int) target;
+ } else {
+ scalledW = (int) target;
+ scalledH = (int) (params.height / ((double) params.width / target));
+ }
+ viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
+ scalledW, scalledH));
+ activity.loadBitmap(message, viewHolder.image);
+ viewHolder.image.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(activity.xmppConnectionService
+ .getFileBackend().getJingleFileUri(message), "image/*");
+ getContext().startActivity(intent);
+ }
+ });
+ viewHolder.image.setOnLongClickListener(new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_STREAM,
+ activity.xmppConnectionService.getFileBackend()
+ .getJingleFileUri(message));
+ shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ shareIntent.setType("image/webp");
+ getContext().startActivity(
+ Intent.createChooser(shareIntent,
+ getContext().getText(R.string.share_with)));
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ final Message item = getItem(position);
+ int type = getItemViewType(position);
+ ViewHolder viewHolder;
+ if (view == null) {
+ viewHolder = new ViewHolder();
+ switch (type) {
+ case NULL:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_null, parent, false);
+ break;
+ case SENT:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_sent, parent, false);
+ viewHolder.message_box = (LinearLayout) view
+ .findViewById(R.id.message_box);
+ viewHolder.contact_picture = (ImageView) view
+ .findViewById(R.id.message_photo);
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(
+ item.getConversation().getAccount(),
+ activity.getPixel(48)));
+ viewHolder.download_button = (Button) view
+ .findViewById(R.id.download_button);
+ viewHolder.indicator = (ImageView) view
+ .findViewById(R.id.security_indicator);
+ viewHolder.image = (ImageView) view
+ .findViewById(R.id.message_image);
+ viewHolder.messageBody = (TextView) view
+ .findViewById(R.id.message_body);
+ viewHolder.time = (TextView) view
+ .findViewById(R.id.message_time);
+ viewHolder.indicatorReceived = (ImageView) view
+ .findViewById(R.id.indicator_received);
+ view.setTag(viewHolder);
+ break;
+ case RECEIVED:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_received, parent, false);
+ viewHolder.message_box = (LinearLayout) view
+ .findViewById(R.id.message_box);
+ viewHolder.contact_picture = (ImageView) view
+ .findViewById(R.id.message_photo);
+ viewHolder.download_button = (Button) view
+ .findViewById(R.id.download_button);
+ if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(item.getContact(),
+ activity.getPixel(48)));
+ }
+ viewHolder.indicator = (ImageView) view
+ .findViewById(R.id.security_indicator);
+ viewHolder.image = (ImageView) view
+ .findViewById(R.id.message_image);
+ viewHolder.messageBody = (TextView) view
+ .findViewById(R.id.message_body);
+ viewHolder.time = (TextView) view
+ .findViewById(R.id.message_time);
+ view.setTag(viewHolder);
+ break;
+ case STATUS:
+ view = (View) activity.getLayoutInflater().inflate(
+ R.layout.message_status, parent, false);
+ viewHolder.contact_picture = (ImageView) view
+ .findViewById(R.id.message_photo);
+ if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
+
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(
+ item.getConversation().getContact(),
+ activity.getPixel(32)));
+ viewHolder.contact_picture.setAlpha(0.5f);
+ viewHolder.contact_picture
+ .setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ String name = item.getConversation()
+ .getName();
+ String read = getContext()
+ .getString(
+ R.string.contact_has_read_up_to_this_point,
+ name);
+ Toast.makeText(getContext(), read,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ }
+ break;
+ default:
+ viewHolder = null;
+ break;
+ }
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+
+ if (type == STATUS) {
+ return view;
+ }
+ if (type == NULL) {
+ if (position == getCount() - 1) {
+ view.getLayoutParams().height = 1;
+ } else {
+ view.getLayoutParams().height = 0;
+
+ }
+ view.setLayoutParams(view.getLayoutParams());
+ return view;
+ }
+
+ if (viewHolder.contact_picture != null) {
+ viewHolder.contact_picture
+ .setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
+ MessageAdapter.this.mOnContactPictureClickedListener
+ .onContactPictureClicked(item);
+ ;
+ }
+
+ }
+ });
+ viewHolder.contact_picture
+ .setOnLongClickListener(new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
+ MessageAdapter.this.mOnContactPictureLongClickedListener
+ .onContactPictureLongClicked(item);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+
+ if (type == RECEIVED) {
+ if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
+ Contact contact = item.getContact();
+ if (contact != null) {
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService()
+ .get(contact, activity.getPixel(48)));
+ } else {
+ String name = item.getPresence();
+ if (name == null) {
+ name = item.getCounterpart();
+ }
+ viewHolder.contact_picture.setImageBitmap(activity
+ .avatarService().get(name, activity.getPixel(48)));
+ }
+ }
+ }
+
+ if (item.getType() == Message.TYPE_IMAGE
+ || item.getDownloadable() != null) {
+ Downloadable d = item.getDownloadable();
+ if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
+ displayInfoMessage(viewHolder, R.string.receiving_image);
+ } else if (d != null
+ && d.getStatus() == Downloadable.STATUS_CHECKING) {
+ displayInfoMessage(viewHolder, R.string.checking_image);
+ } else if (d != null
+ && d.getStatus() == Downloadable.STATUS_DELETED) {
+ displayInfoMessage(viewHolder, R.string.image_file_deleted);
+ } else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) {
+ displayDownloadableMessage(viewHolder, item,
+ R.string.download_image);
+ } else if (d != null
+ && d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
+ displayDownloadableMessage(viewHolder, item,
+ R.string.check_image_filesize);
+ } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED)
+ || (item.getEncryption() == Message.ENCRYPTION_NONE)
+ || (item.getEncryption() == Message.ENCRYPTION_OTR)) {
+ displayImageMessage(viewHolder, item);
+ } else if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+ displayInfoMessage(viewHolder, R.string.encrypted_message);
+ } else {
+ displayDecryptionFailed(viewHolder);
+ }
+ } else {
+ if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+ if (activity.hasPgp()) {
+ displayInfoMessage(viewHolder, R.string.encrypted_message);
+ } else {
+ displayInfoMessage(viewHolder,
+ R.string.install_openkeychain);
+ viewHolder.message_box
+ .setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ activity.showInstallPgpDialog();
+ }
+ });
+ }
+ } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ displayDecryptionFailed(viewHolder);
+ } else {
+ displayTextMessage(viewHolder, item);
+ }
+ }
+
+ displayStatus(viewHolder, item);
+
+ return view;
+ }
+
+ public void startDonwloadable(Message message) {
+ Downloadable downloadable = message.getDownloadable();
+ if (downloadable != null) {
+ if (!downloadable.start()) {
+ Toast.makeText(activity, R.string.not_connected_try_again,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private static class ViewHolder {
+
+ protected LinearLayout message_box;
+ protected Button download_button;
+ protected ImageView image;
+ protected ImageView indicator;
+ protected ImageView indicatorReceived;
+ protected TextView time;
+ protected TextView messageBody;
+ protected ImageView contact_picture;
+
+ }
+
+ public interface OnContactPictureClicked {
+ public void onContactPictureClicked(Message message);
+ }
+
+ public interface OnContactPictureLongClicked {
+ public void onContactPictureLongClicked(Message message);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
new file mode 100644
index 00000000..47595c6e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -0,0 +1,112 @@
+package eu.siacs.conversations.utils;
+
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import android.util.Base64;
+
+public class CryptoHelper {
+ public static final String FILETRANSFER = "?FILETRANSFERv1:";
+ final protected static char[] hexArray = "0123456789abcdef".toCharArray();
+ final protected static char[] vowels = "aeiou".toCharArray();
+ final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz"
+ .toCharArray();
+
+ public static String bytesToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ public static byte[] hexToBytes(String hexString) {
+ int len = hexString.length();
+ byte[] array = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
+ .digit(hexString.charAt(i + 1), 16));
+ }
+ return array;
+ }
+
+ public static String saslPlain(String username, String password) {
+ String sasl = '\u0000' + username + '\u0000' + password;
+ return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()),
+ Base64.NO_WRAP);
+ }
+
+ private static byte[] concatenateByteArrays(byte[] a, byte[] b) {
+ byte[] result = new byte[a.length + b.length];
+ System.arraycopy(a, 0, result, 0, a.length);
+ System.arraycopy(b, 0, result, a.length, b.length);
+ return result;
+ }
+
+ public static String saslDigestMd5(Account account, String challenge,
+ SecureRandom random) {
+ try {
+ String[] challengeParts = new String(Base64.decode(challenge,
+ Base64.DEFAULT)).split(",");
+ String nonce = "";
+ for (int i = 0; i < challengeParts.length; ++i) {
+ String[] parts = challengeParts[i].split("=");
+ if (parts[0].equals("nonce")) {
+ nonce = parts[1].replace("\"", "");
+ } else if (parts[0].equals("rspauth")) {
+ return null;
+ }
+ }
+ String digestUri = "xmpp/" + account.getServer();
+ String nonceCount = "00000001";
+ String x = account.getUsername() + ":" + account.getServer() + ":"
+ + account.getPassword();
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
+ String cNonce = new BigInteger(100, random).toString(32);
+ byte[] a1 = concatenateByteArrays(y,
+ (":" + nonce + ":" + cNonce).getBytes(Charset
+ .defaultCharset()));
+ String a2 = "AUTHENTICATE:" + digestUri;
+ String ha1 = bytesToHex(md.digest(a1));
+ String ha2 = bytesToHex(md.digest(a2.getBytes(Charset
+ .defaultCharset())));
+ String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
+ + ":auth:" + ha2;
+ String response = bytesToHex(md.digest(kd.getBytes(Charset
+ .defaultCharset())));
+ String saslString = "username=\"" + account.getUsername()
+ + "\",realm=\"" + account.getServer() + "\",nonce=\""
+ + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
+ + ",qop=auth,digest-uri=\"" + digestUri + "\",response="
+ + response + ",charset=utf-8";
+ return Base64.encodeToString(
+ saslString.getBytes(Charset.defaultCharset()),
+ Base64.NO_WRAP);
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
+
+ public static String randomMucName(SecureRandom random) {
+ return randomWord(3, random) + "." + randomWord(7, random);
+ }
+
+ protected static String randomWord(int lenght, SecureRandom random) {
+ StringBuilder builder = new StringBuilder(lenght);
+ for (int i = 0; i < lenght; ++i) {
+ if (i % 2 == 0) {
+ builder.append(consonants[random.nextInt(consonants.length)]);
+ } else {
+ builder.append(vowels[random.nextInt(vowels.length)]);
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
new file mode 100644
index 00000000..c51a75ac
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -0,0 +1,185 @@
+package eu.siacs.conversations.utils;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.record.A;
+import de.measite.minidns.record.AAAA;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.util.NameUtil;
+import eu.siacs.conversations.Config;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Random;
+import java.util.TreeMap;
+
+import android.os.Bundle;
+import android.util.Log;
+
+public class DNSHelper {
+ protected static Client client = new Client();
+
+ public static Bundle getSRVRecord(String host) throws IOException {
+ String dns[] = client.findDNS();
+
+ if (dns != null) {
+ for (String dnsserver : dns) {
+ InetAddress ip = InetAddress.getByName(dnsserver);
+ Bundle b = queryDNS(host, ip);
+ if (b.containsKey("name")) {
+ return b;
+ } else if (b.containsKey("error")
+ && "nosrv".equals(b.getString("error", null))) {
+ return b;
+ }
+ }
+ }
+ return queryDNS(host, InetAddress.getByName("8.8.8.8"));
+ }
+
+ public static Bundle queryDNS(String host, InetAddress dnsServer) {
+ Bundle namePort = new Bundle();
+ try {
+ String qname = "_xmpp-client._tcp." + host;
+ Log.d(Config.LOGTAG,
+ "using dns server: " + dnsServer.getHostAddress()
+ + " to look up " + host);
+ DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN,
+ dnsServer.getHostAddress());
+
+ // How should we handle priorities and weight?
+ // Wikipedia has a nice article about priorities vs. weights:
+ // https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability
+
+ // we bucket the SRV records based on priority, pick per priority
+ // a random order respecting the weight, and dump that priority by
+ // priority
+
+ TreeMap<Integer, ArrayList<SRV>> priorities = new TreeMap<Integer, ArrayList<SRV>>();
+ TreeMap<String, ArrayList<String>> ips4 = new TreeMap<String, ArrayList<String>>();
+ TreeMap<String, ArrayList<String>> ips6 = new TreeMap<String, ArrayList<String>>();
+
+ 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<SRV>(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<String>(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<String>(3));
+ }
+ ips6.get(rr.getName()).add("[" + aaaa.toString() + "]");
+ }
+ }
+ }
+
+ Random rnd = new Random();
+ ArrayList<SRV> result = new ArrayList<SRV>(
+ priorities.size() * 2 + 1);
+ for (ArrayList<SRV> s : priorities.values()) {
+
+ // trivial case
+ if (s.size() <= 1) {
+ result.addAll(s);
+ continue;
+ }
+
+ long totalweight = 0l;
+ for (SRV srv : s) {
+ totalweight += srv.getWeight();
+ }
+
+ while (totalweight > 0l && s.size() > 0) {
+ long p = (rnd.nextLong() & 0x7fffffffffffffffl)
+ % totalweight;
+ int i = 0;
+ while (p > 0) {
+ p -= s.get(i++).getPriority();
+ }
+ i--;
+ // remove is expensive, but we have only a few entries
+ // anyway
+ SRV srv = s.remove(i);
+ totalweight -= srv.getWeight();
+ result.add(srv);
+ }
+
+ Collections.shuffle(s, rnd);
+ result.addAll(s);
+
+ }
+
+ if (result.size() == 0) {
+ namePort.putString("error", "nosrv");
+ return namePort;
+ }
+ // we now have a list of servers to try :-)
+
+ // classic name/port pair
+ String resultName = result.get(0).getName();
+ namePort.putString("name", resultName);
+ namePort.putInt("port", result.get(0).getPort());
+
+ if (ips4.containsKey(resultName)) {
+ // we have an ip!
+ ArrayList<String> ip = ips4.get(resultName);
+ Collections.shuffle(ip, rnd);
+ namePort.putString("ipv4", ip.get(0));
+ }
+ if (ips6.containsKey(resultName)) {
+ ArrayList<String> ip = ips6.get(resultName);
+ Collections.shuffle(ip, rnd);
+ namePort.putString("ipv6", ip.get(0));
+ }
+
+ // add all other records
+ int i = 0;
+ for (SRV srv : result) {
+ namePort.putString("name" + i, srv.getName());
+ namePort.putInt("port" + i, srv.getPort());
+ i++;
+ }
+
+ } catch (SocketTimeoutException e) {
+ namePort.putString("error", "timeout");
+ } catch (Exception e) {
+ namePort.putString("error", "unhandled");
+ }
+ return namePort;
+ }
+
+ final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+ public static String bytesToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
new file mode 100644
index 00000000..88fa18ff
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
@@ -0,0 +1,44 @@
+package eu.siacs.conversations.utils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.Thread.UncaughtExceptionHandler;
+
+import android.content.Context;
+
+public class ExceptionHandler implements UncaughtExceptionHandler {
+
+ private UncaughtExceptionHandler defaultHandler;
+ private Context context;
+
+ public ExceptionHandler(Context context) {
+ this.context = context;
+ this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ Writer result = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(result);
+ ex.printStackTrace(printWriter);
+ String stacktrace = result.toString();
+ printWriter.close();
+ try {
+ OutputStream os = context.openFileOutput("stacktrace.txt",
+ Context.MODE_PRIVATE);
+ os.write(stacktrace.getBytes());
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ this.defaultHandler.uncaughtException(thread, ex);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
new file mode 100644
index 00000000..b5fc88bd
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
@@ -0,0 +1,117 @@
+package eu.siacs.conversations.utils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnClickListener;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.preference.PreferenceManager;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+public class ExceptionHelper {
+ public static void init(Context context) {
+ if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(
+ context));
+ }
+ }
+
+ public static void checkForCrash(Context context,
+ final XmppConnectionService service) {
+ try {
+ final SharedPreferences preferences = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ boolean neverSend = preferences.getBoolean("never_send", false);
+ if (neverSend) {
+ return;
+ }
+ List<Account> accounts = service.getAccounts();
+ Account account = null;
+ for (int i = 0; i < accounts.size(); ++i) {
+ if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) {
+ account = accounts.get(i);
+ break;
+ }
+ }
+ if (account == null) {
+ return;
+ }
+ final Account finalAccount = account;
+ FileInputStream file = context.openFileInput("stacktrace.txt");
+ InputStreamReader inputStreamReader = new InputStreamReader(file);
+ BufferedReader stacktrace = new BufferedReader(inputStreamReader);
+ final StringBuilder report = new StringBuilder();
+ PackageManager pm = context.getPackageManager();
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
+ report.append("Version: " + packageInfo.versionName + '\n');
+ report.append("Last Update: "
+ + DateUtils.formatDateTime(context,
+ packageInfo.lastUpdateTime,
+ DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_SHOW_DATE) + '\n');
+ } catch (NameNotFoundException e) {
+ }
+ String line;
+ while ((line = stacktrace.readLine()) != null) {
+ report.append(line);
+ report.append('\n');
+ }
+ file.close();
+ context.deleteFile("stacktrace.txt");
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(context.getString(R.string.crash_report_title));
+ builder.setMessage(context.getText(R.string.crash_report_message));
+ builder.setPositiveButton(context.getText(R.string.send_now),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ Log.d(Config.LOGTAG, "using account="
+ + finalAccount.getJid()
+ + " to send in stack trace");
+ Conversation conversation = service
+ .findOrCreateConversation(finalAccount,
+ "bugs@siacs.eu", false);
+ Message message = new Message(conversation, report
+ .toString(), Message.ENCRYPTION_NONE);
+ service.sendMessage(message);
+ }
+ });
+ builder.setNegativeButton(context.getText(R.string.send_never),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ preferences.edit().putBoolean("never_send", true)
+ .commit();
+ }
+ });
+ builder.create().show();
+ } catch (FileNotFoundException e) {
+ return;
+ } catch (IOException e) {
+ return;
+ }
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
new file mode 100644
index 00000000..9a689768
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.utils;
+
+import java.util.List;
+
+import android.os.Bundle;
+
+public interface OnPhoneContactsLoadedListener {
+ public void onPhoneContactsLoaded(List<Bundle> phoneContacts);
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
new file mode 100644
index 00000000..8fe67234
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
@@ -0,0 +1,327 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial();
+
+ /** Hidden constructor to prevent instantiation. */
+ private PRNGFixes() {
+ }
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException
+ * if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException
+ * if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class
+ .forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException
+ * if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom() throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders = Security
+ .getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class
+ .equals(secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider()
+ .getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider()
+ .getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through all
+ * requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG", 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG (
+ * {@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Log.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException("Failed to read from "
+ + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(new FileInputStream(
+ URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
new file mode 100644
index 00000000..5becc7e7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Profile;
+
+public class PhoneHelper {
+
+ public static void loadPhoneContacts(Context context,
+ final OnPhoneContactsLoadedListener listener) {
+ final List<Bundle> phoneContacts = new ArrayList<Bundle>();
+
+ 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<Cursor>() {
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> arg0, Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+ while (cursor.moveToNext()) {
+ Bundle contact = new Bundle();
+ contact.putInt("phoneid", cursor.getInt(cursor
+ .getColumnIndex(ContactsContract.Data._ID)));
+ contact.putString(
+ "displayname",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
+ contact.putString("photouri", cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.PHOTO_URI)));
+ contact.putString("lookup", cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));
+
+ contact.putString(
+ "jid",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
+ phoneContacts.add(contact);
+ }
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ }
+ });
+ try {
+ mCursorLoader.startLoading();
+ } catch (RejectedExecutionException e) {
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ }
+ }
+
+ public static Uri getSefliUri(Context context) {
+ String[] mProjection = new String[] { Profile._ID, Profile.PHOTO_URI };
+ Cursor mProfileCursor = context.getContentResolver().query(
+ Profile.CONTENT_URI, mProjection, null, null, null);
+
+ if (mProfileCursor == null || mProfileCursor.getCount() == 0) {
+ return null;
+ } else {
+ mProfileCursor.moveToFirst();
+ String uri = mProfileCursor.getString(1);
+ if (uri == null) {
+ return null;
+ } else {
+ return Uri.parse(uri);
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
new file mode 100644
index 00000000..5141c83c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -0,0 +1,225 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class UIHelper {
+ private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
+ private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
+
+ public static String readableTimeDifference(Context context, long time) {
+ return readableTimeDifference(context, time, false);
+ }
+
+ public static String readableTimeDifferenceFull(Context context, long time) {
+ return readableTimeDifference(context, time, true);
+ }
+
+ private static String readableTimeDifference(Context context, long time,
+ boolean fullDate) {
+ if (time == 0) {
+ return context.getString(R.string.just_now);
+ }
+ Date date = new Date(time);
+ long difference = (System.currentTimeMillis() - time) / 1000;
+ if (difference < 60) {
+ return context.getString(R.string.just_now);
+ } else if (difference < 60 * 2) {
+ return context.getString(R.string.minute_ago);
+ } else if (difference < 60 * 15) {
+ return context.getString(R.string.minutes_ago,
+ Math.round(difference / 60.0));
+ } else if (today(date)) {
+ java.text.DateFormat df = DateFormat.getTimeFormat(context);
+ return df.format(date);
+ } else {
+ if (fullDate) {
+ return DateUtils.formatDateTime(context, date.getTime(),
+ FULL_DATE_FLAGS);
+ } else {
+ return DateUtils.formatDateTime(context, date.getTime(),
+ SHORT_DATE_FLAGS);
+ }
+ }
+ }
+
+ private static boolean today(Date date) {
+ Calendar cal1 = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ cal1.setTime(date);
+ cal2.setTimeInMillis(System.currentTimeMillis());
+ return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
+ && cal1.get(Calendar.DAY_OF_YEAR) == cal2
+ .get(Calendar.DAY_OF_YEAR);
+ }
+
+ public static String lastseen(Context context, long time) {
+ if (time == 0) {
+ return context.getString(R.string.never_seen);
+ }
+ long difference = (System.currentTimeMillis() - time) / 1000;
+ if (difference < 60) {
+ return context.getString(R.string.last_seen_now);
+ } else if (difference < 60 * 2) {
+ return context.getString(R.string.last_seen_min);
+ } else if (difference < 60 * 60) {
+ return context.getString(R.string.last_seen_mins,
+ Math.round(difference / 60.0));
+ } else if (difference < 60 * 60 * 2) {
+ return context.getString(R.string.last_seen_hour);
+ } else if (difference < 60 * 60 * 24) {
+ return context.getString(R.string.last_seen_hours,
+ Math.round(difference / (60.0 * 60.0)));
+ } else if (difference < 60 * 60 * 48) {
+ return context.getString(R.string.last_seen_day);
+ } else {
+ return context.getString(R.string.last_seen_days,
+ Math.round(difference / (60.0 * 60.0 * 24.0)));
+ }
+ }
+
+ public static void showErrorNotification(Context context,
+ List<Account> accounts) {
+ NotificationManager mNotificationManager = (NotificationManager) context
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ List<Account> accountsWproblems = new ArrayList<Account>();
+ for (Account account : accounts) {
+ if (account.hasErrorStatus()) {
+ accountsWproblems.add(account);
+ }
+ }
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
+ context);
+ if (accountsWproblems.size() == 0) {
+ mNotificationManager.cancel(1111);
+ return;
+ } else if (accountsWproblems.size() == 1) {
+ mBuilder.setContentTitle(context
+ .getString(R.string.problem_connecting_to_account));
+ mBuilder.setContentText(accountsWproblems.get(0).getJid());
+ } else {
+ mBuilder.setContentTitle(context
+ .getString(R.string.problem_connecting_to_accounts));
+ mBuilder.setContentText(context.getString(R.string.touch_to_fix));
+ }
+ mBuilder.setOngoing(true);
+ mBuilder.setLights(0xffffffff, 2000, 4000);
+ mBuilder.setSmallIcon(R.drawable.ic_notification);
+ TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ Intent manageAccountsIntent = new Intent(context,
+ ManageAccountActivity.class);
+ stackBuilder.addNextIntent(manageAccountsIntent);
+
+ PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ mBuilder.setContentIntent(resultPendingIntent);
+ Notification notification = mBuilder.build();
+ mNotificationManager.notify(1111, notification);
+ }
+
+ @SuppressLint("InflateParams")
+ public static AlertDialog getVerifyFingerprintDialog(
+ final ConversationActivity activity,
+ final Conversation conversation, final View msg) {
+ final Contact contact = conversation.getContact();
+ final Account account = conversation.getAccount();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle("Verify fingerprint");
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.dialog_verify_otr, null);
+ TextView jid = (TextView) view.findViewById(R.id.verify_otr_jid);
+ TextView fingerprint = (TextView) view
+ .findViewById(R.id.verify_otr_fingerprint);
+ TextView yourprint = (TextView) view
+ .findViewById(R.id.verify_otr_yourprint);
+
+ jid.setText(contact.getJid());
+ fingerprint.setText(conversation.getOtrFingerprint());
+ yourprint.setText(account.getOtrFingerprint());
+ builder.setNegativeButton("Cancel", null);
+ builder.setPositiveButton("Verify", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ contact.addOtrFingerprint(conversation.getOtrFingerprint());
+ msg.setVisibility(View.GONE);
+ activity.xmppConnectionService.syncRosterToDisk(account);
+ }
+ });
+ builder.setView(view);
+ return builder.create();
+ }
+
+ private final static class EmoticonPattern {
+ Pattern pattern;
+ String replacement;
+
+ EmoticonPattern(String ascii, int unicode) {
+ this.pattern = Pattern.compile("(?<=(^|\\s))" + ascii
+ + "(?=(\\s|$))");
+ this.replacement = new String(new int[] { unicode, }, 0, 1);
+ }
+
+ String replaceAll(String body) {
+ return pattern.matcher(body).replaceAll(replacement);
+ }
+ }
+
+ private static final EmoticonPattern[] patterns = new EmoticonPattern[] {
+ new EmoticonPattern(":-?D", 0x1f600),
+ new EmoticonPattern("\\^\\^", 0x1f601),
+ new EmoticonPattern(":'D", 0x1f602),
+ new EmoticonPattern("\\]-?D", 0x1f608),
+ new EmoticonPattern(";-?\\)", 0x1f609),
+ new EmoticonPattern(":-?\\)", 0x1f60a),
+ new EmoticonPattern("[B8]-?\\)", 0x1f60e),
+ new EmoticonPattern(":-?\\|", 0x1f610),
+ new EmoticonPattern(":-?[/\\\\]", 0x1f615),
+ new EmoticonPattern(":-?\\*", 0x1f617),
+ new EmoticonPattern(":-?[Ppb]", 0x1f61b),
+ new EmoticonPattern(":-?\\(", 0x1f61e),
+ new EmoticonPattern(":-?[0Oo]", 0x1f62e),
+ new EmoticonPattern("\\\\o/", 0x1F631), };
+
+ public static String transformAsciiEmoticons(String body) {
+ if (body != null) {
+ for (EmoticonPattern p : patterns) {
+ body = p.replaceAll(body);
+ }
+ body = body.trim();
+ }
+ return body;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/Validator.java b/src/main/java/eu/siacs/conversations/utils/Validator.java
new file mode 100644
index 00000000..00130fa2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/Validator.java
@@ -0,0 +1,14 @@
+package eu.siacs.conversations.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Validator {
+ public static final Pattern VALID_JID = Pattern.compile(
+ "^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE);
+
+ public static boolean isValidJid(String jid) {
+ Matcher matcher = VALID_JID.matcher(jid);
+ return matcher.find();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java
new file mode 100644
index 00000000..4dee07cf
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java
@@ -0,0 +1,12 @@
+package eu.siacs.conversations.utils;
+
+public class XmlHelper {
+ public static String encodeEntities(String content) {
+ content = content.replace("&", "&amp;");
+ content = content.replace("<", "&lt;");
+ content = content.replace(">", "&gt;");
+ content = content.replace("\"", "&quot;");
+ content = content.replace("'", "&apos;");
+ return content;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java
new file mode 100644
index 00000000..b777c10c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java
@@ -0,0 +1,54 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * ZLibInputStream is a zlib and input stream compatible version of an
+ * InflaterInputStream. This class solves the incompatibility between
+ * {@link InputStream#available()} and {@link InflaterInputStream#available()}.
+ */
+public class ZLibInputStream extends InflaterInputStream {
+
+ /**
+ * Construct a ZLibInputStream, reading data from the underlying stream.
+ *
+ * @param is
+ * The {@code InputStream} to read data from.
+ * @throws IOException
+ * If an {@code IOException} occurs.
+ */
+ public ZLibInputStream(InputStream is) throws IOException {
+ super(is, new Inflater(), 512);
+ }
+
+ /**
+ * Provide a more InputStream compatible version of available. A return
+ * value of 1 means that it is likly to read one byte without blocking, 0
+ * means that the system is known to block for more input.
+ *
+ * @return 0 if no data is available, 1 otherwise
+ * @throws IOException
+ */
+ @Override
+ public int available() throws IOException {
+ /*
+ * This is one of the funny code blocks. InflaterInputStream.available
+ * violates the contract of InputStream.available, which breaks kXML2.
+ *
+ * I'm not sure who's to blame, oracle/sun for a broken api or the
+ * google guys for mixing a sun bug with a xml reader that can't handle
+ * it....
+ *
+ * Anyway, this simple if breaks suns distorted reality, but helps to
+ * use the api as intended.
+ */
+ if (inf.needsInput()) {
+ return 0;
+ }
+ return super.available();
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java
new file mode 100644
index 00000000..8b3f5e68
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.NoSuchAlgorithmException;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+/**
+ * <p>
+ * Android 2.2 includes Java7 FLUSH_SYNC option, which will be used by this
+ * Implementation, preferable via reflection. The @hide was remove in API level
+ * 19. This class might thus go away in the future.
+ * </p>
+ * <p>
+ * Please use {@link ZLibOutputStream#SUPPORTED} to check for flush
+ * compatibility.
+ * </p>
+ */
+public class ZLibOutputStream extends DeflaterOutputStream {
+
+ /**
+ * The reflection based flush method.
+ */
+
+ private final static Method method;
+ /**
+ * SUPPORTED is true if a flush compatible method exists.
+ */
+ public final static boolean SUPPORTED;
+
+ /**
+ * Static block to initialize {@link #SUPPORTED} and {@link #method}.
+ */
+ static {
+ Method m = null;
+ try {
+ m = Deflater.class.getMethod("deflate", byte[].class, int.class,
+ int.class, int.class);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ }
+ method = m;
+ SUPPORTED = (method != null);
+ }
+
+ /**
+ * Create a new ZLib compatible output stream wrapping the given low level
+ * stream. ZLib compatiblity means we will send a zlib header.
+ *
+ * @param os
+ * OutputStream The underlying stream.
+ * @throws IOException
+ * In case of a lowlevel transfer problem.
+ * @throws NoSuchAlgorithmException
+ * In case of a {@link Deflater} error.
+ */
+ public ZLibOutputStream(OutputStream os) throws IOException,
+ NoSuchAlgorithmException {
+ super(os, new Deflater(Deflater.BEST_COMPRESSION));
+ }
+
+ /**
+ * Flush the given stream, preferring Java7 FLUSH_SYNC if available.
+ *
+ * @throws IOException
+ * In case of a lowlevel exception.
+ */
+ @Override
+ public void flush() throws IOException {
+ if (!SUPPORTED) {
+ super.flush();
+ return;
+ }
+ try {
+ int count = 0;
+ do {
+ count = (Integer) method.invoke(def, buf, 0, buf.length, 3);
+ if (count > 0) {
+ out.write(buf, 0, count);
+ }
+ } while (count > 0);
+ } catch (IllegalArgumentException e) {
+ throw new IOException("Can't flush");
+ } catch (IllegalAccessException e) {
+ throw new IOException("Can't flush");
+ } catch (InvocationTargetException e) {
+ throw new IOException("Can't flush");
+ }
+ super.flush();
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java
new file mode 100644
index 00000000..4e11ee2c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/Element.java
@@ -0,0 +1,148 @@
+package eu.siacs.conversations.xml;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+import eu.siacs.conversations.utils.XmlHelper;
+
+public class Element {
+ protected String name;
+ protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+ protected String content;
+ protected List<Element> children = new ArrayList<Element>();
+
+ public Element(String name) {
+ this.name = name;
+ }
+
+ public Element addChild(Element child) {
+ this.content = null;
+ children.add(child);
+ return child;
+ }
+
+ public Element addChild(String name) {
+ this.content = null;
+ Element child = new Element(name);
+ children.add(child);
+ return child;
+ }
+
+ public Element addChild(String name, String xmlns) {
+ this.content = null;
+ Element child = new Element(name);
+ child.setAttribute("xmlns", xmlns);
+ children.add(child);
+ return child;
+ }
+
+ public Element setContent(String content) {
+ this.content = content;
+ this.children.clear();
+ return this;
+ }
+
+ public Element findChild(String name) {
+ for (Element child : this.children) {
+ if (child.getName().equals(name)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public Element findChild(String name, String xmlns) {
+ for (Element child : this.children) {
+ if (child.getName().equals(name)
+ && (child.getAttribute("xmlns").equals(xmlns))) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public boolean hasChild(String name) {
+ return findChild(name) != null;
+ }
+
+ public boolean hasChild(String name, String xmlns) {
+ return findChild(name, xmlns) != null;
+ }
+
+ public List<Element> getChildren() {
+ return this.children;
+ }
+
+ public Element setChildren(List<Element> 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<String, String> attributes) {
+ this.attributes = attributes;
+ return this;
+ }
+
+ public String getAttribute(String name) {
+ if (this.attributes.containsKey(name)) {
+ return this.attributes.get(name);
+ } else {
+ return null;
+ }
+ }
+
+ public Hashtable<String, String> getAttributes() {
+ return this.attributes;
+ }
+
+ public String toString() {
+ StringBuilder elementOutput = new StringBuilder();
+ if ((content == null) && (children.size() == 0)) {
+ Tag emptyTag = Tag.empty(name);
+ emptyTag.setAtttributes(this.attributes);
+ elementOutput.append(emptyTag.toString());
+ } else {
+ Tag startTag = Tag.start(name);
+ startTag.setAtttributes(this.attributes);
+ elementOutput.append(startTag);
+ if (content != null) {
+ elementOutput.append(XmlHelper.encodeEntities(content));
+ } else {
+ for (Element child : children) {
+ elementOutput.append(child.toString());
+ }
+ }
+ Tag endTag = Tag.end(name);
+ elementOutput.append(endTag);
+ }
+ return elementOutput.toString();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void clearChildren() {
+ this.children.clear();
+ }
+
+ public void setAttribute(String name, long value) {
+ this.setAttribute(name, Long.toString(value));
+ }
+
+ public void setAttribute(String name, int value) {
+ this.setAttribute(name, Integer.toString(value));
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java
new file mode 100644
index 00000000..b9ef979f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/Tag.java
@@ -0,0 +1,104 @@
+package eu.siacs.conversations.xml;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import eu.siacs.conversations.utils.XmlHelper;
+
+public class Tag {
+ public static final int NO = -1;
+ public static final int START = 0;
+ public static final int END = 1;
+ public static final int EMPTY = 2;
+
+ protected int type;
+ protected String name;
+ protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+
+ 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<String, String> 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<Entry<String, String>> attributeSet = attributes.entrySet();
+ Iterator<Entry<String, String>> it = attributeSet.iterator();
+ while (it.hasNext()) {
+ Entry<String, String> 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<String, String> 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
new file mode 100644
index 00000000..f11c1846
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java
@@ -0,0 +1,114 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class TagWriter {
+
+ private OutputStream plainOutputStream;
+ private OutputStreamWriter outputStream;
+ private boolean finshed = false;
+ private LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>();
+ 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("<?xml version='1.0'?>");
+ 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
new file mode 100644
index 00000000..52d3d46a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java
@@ -0,0 +1,141 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import eu.siacs.conversations.Config;
+
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
+
+public class XmlReader {
+ private XmlPullParser parser;
+ private PowerManager.WakeLock wakeLock;
+ private InputStream is;
+
+ public XmlReader(WakeLock wakeLock) {
+ this.parser = Xml.newPullParser();
+ try {
+ this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
+ true);
+ } catch (XmlPullParserException e) {
+ Log.d(Config.LOGTAG, "error setting namespace feature on parser");
+ }
+ this.wakeLock = wakeLock;
+ }
+
+ public void setInputStream(InputStream inputStream) throws IOException {
+ if (inputStream == null) {
+ throw new IOException();
+ }
+ this.is = inputStream;
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ throw new IOException("error resetting parser");
+ }
+ }
+
+ public InputStream getInputStream() throws IOException {
+ if (this.is == null) {
+ throw new IOException();
+ }
+ return is;
+ }
+
+ public void reset() throws IOException {
+ if (this.is == null) {
+ throw new IOException();
+ }
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ throw new IOException("error resetting parser");
+ }
+ }
+
+ public Tag readTag() throws XmlPullParserException, IOException {
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ try {
+ while (this.is != null
+ && parser.next() != XmlPullParser.END_DOCUMENT) {
+ wakeLock.acquire();
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ Tag tag = Tag.start(parser.getName());
+ for (int i = 0; i < parser.getAttributeCount(); ++i) {
+ tag.setAttribute(parser.getAttributeName(i),
+ parser.getAttributeValue(i));
+ }
+ String xmlns = parser.getNamespace();
+ if (xmlns != null) {
+ tag.setAttribute("xmlns", xmlns);
+ }
+ return tag;
+ } else if (parser.getEventType() == XmlPullParser.END_TAG) {
+ Tag tag = Tag.end(parser.getName());
+ return tag;
+ } else if (parser.getEventType() == XmlPullParser.TEXT) {
+ Tag tag = Tag.no(parser.getText());
+ return tag;
+ }
+ }
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new IOException(
+ "xml parser mishandled ArrayIndexOufOfBounds", e);
+ } catch (StringIndexOutOfBoundsException e) {
+ throw new IOException(
+ "xml parser mishandled StringIndexOufOfBounds", e);
+ } catch (NullPointerException e) {
+ throw new IOException("xml parser mishandled NullPointerException",
+ e);
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("xml parser mishandled IndexOutOfBound", e);
+ }
+ return null;
+ }
+
+ public Element readElement(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ Element element = new Element(currentTag.getName());
+ element.setAttributes(currentTag.getAttributes());
+ Tag nextTag = this.readTag();
+ if (nextTag == null) {
+ throw new IOException("unterupted mid tag");
+ }
+ if (nextTag.isNo()) {
+ element.setContent(nextTag.getName());
+ nextTag = this.readTag();
+ if (nextTag == null) {
+ throw new IOException("unterupted mid tag");
+ }
+ }
+ while (!nextTag.isEnd(element.getName())) {
+ if (!nextTag.isNo()) {
+ Element child = this.readElement(nextTag);
+ element.addChild(child);
+ }
+ nextTag = this.readTag();
+ if (nextTag == null) {
+ throw new IOException("unterupted mid tag");
+ }
+ }
+ return element;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java
new file mode 100644
index 00000000..f09cf33d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnBindListener {
+ public void onBind(Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java
new file mode 100644
index 00000000..849e8e76
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Contact;
+
+public interface OnContactStatusChanged {
+ public void onContactStatusChanged(Contact contact, boolean online);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
new file mode 100644
index 00000000..a4cff986
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public interface OnIqPacketReceived extends PacketReceived {
+ public void onIqPacketReceived(Account account, IqPacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java
new file mode 100644
index 00000000..5f670d93
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnMessageAcknowledged {
+ public void onMessageAcknowledged(Account account, String id);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
new file mode 100644
index 00000000..325e945f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public interface OnMessagePacketReceived extends PacketReceived {
+ public void onMessagePacketReceived(Account account, MessagePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
new file mode 100644
index 00000000..95c1acfc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public interface OnPresencePacketReceived extends PacketReceived {
+ public void onPresencePacketReceived(Account account, PresencePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java
new file mode 100644
index 00000000..ad1d98cb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnStatusChanged {
+ public void onStatusChanged(Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java
new file mode 100644
index 00000000..d4502d73
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.xmpp;
+
+public abstract interface PacketReceived {
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
new file mode 100644
index 00000000..903dc59d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -0,0 +1,1130 @@
+package eu.siacs.conversations.xmpp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.xmlpull.v1.XmlPullParserException;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.util.SparseArray;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.DNSHelper;
+import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
+import eu.siacs.conversations.utils.zlib.ZLibInputStream;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Tag;
+import eu.siacs.conversations.xml.TagWriter;
+import eu.siacs.conversations.xml.XmlReader;
+import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
+
+public class XmppConnection implements Runnable {
+
+ protected Account account;
+
+ private WakeLock wakeLock;
+
+ private SecureRandom mRandom;
+
+ private Socket socket;
+ private XmlReader tagReader;
+ private TagWriter tagWriter;
+
+ private Features features = new Features(this);
+
+ private boolean shouldBind = true;
+ private boolean shouldAuthenticate = true;
+ private Element streamFeatures;
+ private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
+
+ private String streamId = null;
+ private int smVersion = 3;
+ private SparseArray<String> messageReceipts = new SparseArray<String>();
+
+ private boolean usingCompression = false;
+ private boolean usingEncryption = false;
+
+ private int stanzasReceived = 0;
+ private int stanzasSent = 0;
+
+ private long lastPaketReceived = 0;
+ private long lastPingSent = 0;
+ private long lastConnect = 0;
+ private long lastSessionStarted = 0;
+
+ private int attempt = 0;
+
+ private static final int PACKET_IQ = 0;
+ private static final int PACKET_MESSAGE = 1;
+ private static final int PACKET_PRESENCE = 2;
+
+ private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
+ private OnPresencePacketReceived presenceListener = null;
+ private OnJinglePacketReceived jingleListener = null;
+ private OnIqPacketReceived unregisteredIqListener = null;
+ private OnMessagePacketReceived messageListener = null;
+ private OnStatusChanged statusListener = null;
+ private OnBindListener bindListener = null;
+ private OnMessageAcknowledged acknowledgedListener = null;
+ private MemorizingTrustManager mMemorizingTrustManager;
+ private final Context applicationContext;
+
+ public XmppConnection(Account account, XmppConnectionService service) {
+ this.mRandom = service.getRNG();
+ this.mMemorizingTrustManager = service.getMemorizingTrustManager();
+ this.account = account;
+ this.wakeLock = service.getPowerManager().newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
+ tagWriter = new TagWriter();
+ applicationContext = service.getApplicationContext();
+ }
+
+ protected void changeStatus(int nextStatus) {
+ if (account.getStatus() != nextStatus) {
+ if ((nextStatus == Account.STATUS_OFFLINE)
+ && (account.getStatus() != Account.STATUS_CONNECTING)
+ && (account.getStatus() != Account.STATUS_ONLINE)
+ && (account.getStatus() != Account.STATUS_DISABLED)) {
+ return;
+ }
+ if (nextStatus == Account.STATUS_ONLINE) {
+ this.attempt = 0;
+ }
+ account.setStatus(nextStatus);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ }
+
+ protected void connect() {
+ Log.d(Config.LOGTAG, account.getJid() + ": connecting");
+ usingCompression = false;
+ usingEncryption = false;
+ lastConnect = SystemClock.elapsedRealtime();
+ lastPingSent = SystemClock.elapsedRealtime();
+ this.attempt++;
+ try {
+ shouldAuthenticate = shouldBind = !account
+ .isOptionSet(Account.OPTION_REGISTER);
+ tagReader = new XmlReader(wakeLock);
+ tagWriter = new TagWriter();
+ packetCallbacks.clear();
+ this.changeStatus(Account.STATUS_CONNECTING);
+ Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
+ if ("timeout".equals(namePort.getString("error"))) {
+ Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
+ this.changeStatus(Account.STATUS_OFFLINE);
+ return;
+ }
+ String srvRecordServer = namePort.getString("name");
+ String srvIpServer = namePort.getString("ipv4");
+ int srvRecordPort = namePort.getInt("port");
+ if (srvRecordServer != null) {
+ if (srvIpServer != null) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": using values from dns " + srvRecordServer
+ + "[" + srvIpServer + "]:" + srvRecordPort);
+ socket = new Socket(srvIpServer, srvRecordPort);
+ } else {
+ boolean socketError = true;
+ int srvIndex = 0;
+ while (socketError
+ && namePort.containsKey("name" + srvIndex)) {
+ try {
+ srvRecordServer = namePort.getString("name"
+ + srvIndex);
+ srvRecordPort = namePort.getInt("port" + srvIndex);
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": using values from dns "
+ + srvRecordServer + ":" + srvRecordPort);
+ socket = new Socket(srvRecordServer, srvRecordPort);
+ socketError = false;
+ } catch (UnknownHostException e) {
+ srvIndex++;
+ if (!namePort.containsKey("name" + srvIndex)) {
+ throw e;
+ }
+ } catch (IOException e) {
+ srvIndex++;
+ if (!namePort.containsKey("name" + srvIndex)) {
+ throw e;
+ }
+ }
+ }
+ }
+ } else if (namePort.containsKey("error")
+ && "nosrv".equals(namePort.getString("error", null))) {
+ socket = new Socket(account.getServer(), 5222);
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": timeout in DNS resolution");
+ changeStatus(Account.STATUS_OFFLINE);
+ return;
+ }
+ OutputStream out = socket.getOutputStream();
+ tagWriter.setOutputStream(out);
+ InputStream in = socket.getInputStream();
+ tagReader.setInputStream(in);
+ tagWriter.beginDocument();
+ sendStartStream();
+ Tag nextTag;
+ while ((nextTag = tagReader.readTag()) != null) {
+ if (nextTag.isStart("stream")) {
+ processStream(nextTag);
+ break;
+ } else {
+ Log.d(Config.LOGTAG,
+ "found unexpected tag: " + nextTag.getName());
+ return;
+ }
+ }
+ if (socket.isConnected()) {
+ socket.close();
+ }
+ } catch (UnknownHostException e) {
+ this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ } catch (IOException e) {
+ this.changeStatus(Account.STATUS_OFFLINE);
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ } catch (NoSuchAlgorithmException e) {
+ this.changeStatus(Account.STATUS_OFFLINE);
+ Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ } catch (XmlPullParserException e) {
+ this.changeStatus(Account.STATUS_OFFLINE);
+ Log.d(Config.LOGTAG, "xml exception " + e.getMessage());
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (RuntimeException re) {
+ }
+ }
+ return;
+ }
+
+ }
+
+ @Override
+ public void run() {
+ connect();
+ }
+
+ private void processStream(Tag currentTag) throws XmlPullParserException,
+ IOException, NoSuchAlgorithmException {
+ Tag nextTag = tagReader.readTag();
+ while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
+ if (nextTag.isStart("error")) {
+ processStreamError(nextTag);
+ } else if (nextTag.isStart("features")) {
+ processStreamFeatures(nextTag);
+ } else if (nextTag.isStart("proceed")) {
+ switchOverToTls(nextTag);
+ } else if (nextTag.isStart("compressed")) {
+ switchOverToZLib(nextTag);
+ } else if (nextTag.isStart("success")) {
+ Log.d(Config.LOGTAG, account.getJid() + ": logged in");
+ tagReader.readTag();
+ tagReader.reset();
+ sendStartStream();
+ processStream(tagReader.readTag());
+ break;
+ } else if (nextTag.isStart("failure")) {
+ tagReader.readElement(nextTag);
+ changeStatus(Account.STATUS_UNAUTHORIZED);
+ } else if (nextTag.isStart("challenge")) {
+ String challange = tagReader.readElement(nextTag).getContent();
+ Element response = new Element("response");
+ response.setAttribute("xmlns",
+ "urn:ietf:params:xml:ns:xmpp-sasl");
+ response.setContent(CryptoHelper.saslDigestMd5(account,
+ challange, mRandom));
+ tagWriter.writeElement(response);
+ } else if (nextTag.isStart("enabled")) {
+ Element enabled = tagReader.readElement(nextTag);
+ if ("true".equals(enabled.getAttribute("resume"))) {
+ this.streamId = enabled.getAttribute("id");
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": stream managment(" + smVersion
+ + ") enabled (resumable)");
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": stream managment(" + smVersion + ") enabled");
+ }
+ this.lastSessionStarted = SystemClock.elapsedRealtime();
+ this.stanzasReceived = 0;
+ RequestPacket r = new RequestPacket(smVersion);
+ tagWriter.writeStanzaAsync(r);
+ } else if (nextTag.isStart("resumed")) {
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ Element resumed = tagReader.readElement(nextTag);
+ String h = resumed.getAttribute("h");
+ try {
+ int serverCount = Integer.parseInt(h);
+ if (serverCount != stanzasSent) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": session resumed with lost packages");
+ stanzasSent = serverCount;
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": session resumed");
+ }
+ if (acknowledgedListener != null) {
+ for (int i = 0; i < messageReceipts.size(); ++i) {
+ if (serverCount >= messageReceipts.keyAt(i)) {
+ acknowledgedListener.onMessageAcknowledged(
+ account, messageReceipts.valueAt(i));
+ }
+ }
+ }
+ messageReceipts.clear();
+ } catch (NumberFormatException e) {
+
+ }
+ sendInitialPing();
+
+ } else if (nextTag.isStart("r")) {
+ tagReader.readElement(nextTag);
+ AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
+ tagWriter.writeStanzaAsync(ack);
+ } else if (nextTag.isStart("a")) {
+ Element ack = tagReader.readElement(nextTag);
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ int serverSequence = Integer.parseInt(ack.getAttribute("h"));
+ String msgId = this.messageReceipts.get(serverSequence);
+ if (msgId != null) {
+ if (this.acknowledgedListener != null) {
+ this.acknowledgedListener.onMessageAcknowledged(
+ account, msgId);
+ }
+ this.messageReceipts.remove(serverSequence);
+ }
+ } else if (nextTag.isStart("failed")) {
+ tagReader.readElement(nextTag);
+ Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
+ streamId = null;
+ if (account.getStatus() != Account.STATUS_ONLINE) {
+ sendBindRequest();
+ }
+ } else if (nextTag.isStart("iq")) {
+ processIq(nextTag);
+ } else if (nextTag.isStart("message")) {
+ processMessage(nextTag);
+ } else if (nextTag.isStart("presence")) {
+ processPresence(nextTag);
+ }
+ nextTag = tagReader.readTag();
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ account.setStatus(Account.STATUS_OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ }
+
+ private void sendInitialPing() {
+ Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping");
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setFrom(account.getFullJid());
+ iq.addChild("ping", "urn:xmpp:ping");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": online with resource " + account.getResource());
+ changeStatus(Account.STATUS_ONLINE);
+ }
+ });
+ }
+
+ private Element processPacket(Tag currentTag, int packetType)
+ throws XmlPullParserException, IOException {
+ Element element;
+ switch (packetType) {
+ case PACKET_IQ:
+ element = new IqPacket();
+ break;
+ case PACKET_MESSAGE:
+ element = new MessagePacket();
+ break;
+ case PACKET_PRESENCE:
+ element = new PresencePacket();
+ break;
+ default:
+ return null;
+ }
+ element.setAttributes(currentTag.getAttributes());
+ Tag nextTag = tagReader.readTag();
+ if (nextTag == null) {
+ throw new IOException("interrupted mid tag");
+ }
+ while (!nextTag.isEnd(element.getName())) {
+ if (!nextTag.isNo()) {
+ Element child = tagReader.readElement(nextTag);
+ String type = currentTag.getAttribute("type");
+ if (packetType == PACKET_IQ
+ && "jingle".equals(child.getName())
+ && ("set".equalsIgnoreCase(type) || "get"
+ .equalsIgnoreCase(type))) {
+ element = new JinglePacket();
+ element.setAttributes(currentTag.getAttributes());
+ }
+ element.addChild(child);
+ }
+ nextTag = tagReader.readTag();
+ if (nextTag == null) {
+ throw new IOException("interrupted mid tag");
+ }
+ }
+ ++stanzasReceived;
+ lastPaketReceived = SystemClock.elapsedRealtime();
+ return element;
+ }
+
+ private void processIq(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
+
+ if (packet.getId() == null) {
+ return; // an iq packet without id is definitely invalid
+ }
+
+ if (packet instanceof JinglePacket) {
+ if (this.jingleListener != null) {
+ this.jingleListener.onJinglePacketReceived(account,
+ (JinglePacket) packet);
+ }
+ } else {
+ if (packetCallbacks.containsKey(packet.getId())) {
+ if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
+ ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
+ .onIqPacketReceived(account, packet);
+ }
+
+ packetCallbacks.remove(packet.getId());
+ } else if ((packet.getType() == IqPacket.TYPE_GET || packet
+ .getType() == IqPacket.TYPE_SET)
+ && this.unregisteredIqListener != null) {
+ this.unregisteredIqListener.onIqPacketReceived(account, packet);
+ }
+ }
+ }
+
+ private void processMessage(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ MessagePacket packet = (MessagePacket) processPacket(currentTag,
+ PACKET_MESSAGE);
+ String id = packet.getAttribute("id");
+ if ((id != null) && (packetCallbacks.containsKey(id))) {
+ if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
+ ((OnMessagePacketReceived) packetCallbacks.get(id))
+ .onMessagePacketReceived(account, packet);
+ }
+ packetCallbacks.remove(id);
+ } else if (this.messageListener != null) {
+ this.messageListener.onMessagePacketReceived(account, packet);
+ }
+ }
+
+ private void processPresence(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ PresencePacket packet = (PresencePacket) processPacket(currentTag,
+ PACKET_PRESENCE);
+ String id = packet.getAttribute("id");
+ if ((id != null) && (packetCallbacks.containsKey(id))) {
+ if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
+ ((OnPresencePacketReceived) packetCallbacks.get(id))
+ .onPresencePacketReceived(account, packet);
+ }
+ packetCallbacks.remove(id);
+ } else if (this.presenceListener != null) {
+ this.presenceListener.onPresencePacketReceived(account, packet);
+ }
+ }
+
+ private void sendCompressionZlib() throws IOException {
+ Element compress = new Element("compress");
+ compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
+ compress.addChild("method").setContent("zlib");
+ tagWriter.writeElement(compress);
+ }
+
+ private void switchOverToZLib(Tag currentTag)
+ throws XmlPullParserException, IOException,
+ NoSuchAlgorithmException {
+ tagReader.readTag(); // read tag close
+ tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
+ .getOutputStream()));
+ tagReader
+ .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
+
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
+ usingCompression = true;
+ processStream(tagReader.readTag());
+ }
+
+ private void sendStartTLS() throws IOException {
+ Tag startTLS = Tag.empty("starttls");
+ startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
+ tagWriter.writeTag(startTLS);
+ }
+
+ private SharedPreferences getPreferences() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(applicationContext);
+ }
+
+ private boolean enableLegacySSL() {
+ return getPreferences().getBoolean("enable_legacy_ssl", false);
+ }
+
+ private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ tagReader.readTag();
+ try {
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null,
+ new X509TrustManager[] { this.mMemorizingTrustManager },
+ mRandom);
+ SSLSocketFactory factory = sc.getSocketFactory();
+
+ HostnameVerifier verifier = this.mMemorizingTrustManager
+ .wrapHostnameVerifier(new StrictHostnameVerifier());
+ SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
+ socket.getInetAddress().getHostAddress(), socket.getPort(),
+ true);
+
+ // Support all protocols except legacy SSL.
+ // The min SDK version prevents us having to worry about SSLv2. In
+ // future, this may be
+ // true of SSLv3 as well.
+ final String[] supportProtocols;
+ if (enableLegacySSL()) {
+ supportProtocols = sslSocket.getSupportedProtocols();
+ } else {
+ final List<String> supportedProtocols = new LinkedList<String>(
+ Arrays.asList(sslSocket.getSupportedProtocols()));
+ supportedProtocols.remove("SSLv3");
+ supportProtocols = new String[supportedProtocols.size()];
+ supportedProtocols.toArray(supportProtocols);
+ }
+ sslSocket.setEnabledProtocols(supportProtocols);
+
+ if (verifier != null
+ && !verifier.verify(account.getServer(),
+ sslSocket.getSession())) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": host mismatch in TLS connection");
+ sslSocket.close();
+ throw new IOException();
+ }
+ tagReader.setInputStream(sslSocket.getInputStream());
+ tagWriter.setOutputStream(sslSocket.getOutputStream());
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": TLS connection established");
+ usingEncryption = true;
+ processStream(tagReader.readTag());
+ sslSocket.close();
+ } catch (NoSuchAlgorithmException e1) {
+ e1.printStackTrace();
+ } catch (KeyManagementException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendSaslAuthPlain() throws IOException {
+ String saslString = CryptoHelper.saslPlain(account.getUsername(),
+ account.getPassword());
+ Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ auth.setAttribute("mechanism", "PLAIN");
+ auth.setContent(saslString);
+ tagWriter.writeElement(auth);
+ }
+
+ private void sendSaslAuthDigestMd5() throws IOException {
+ Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ auth.setAttribute("mechanism", "DIGEST-MD5");
+ tagWriter.writeElement(auth);
+ }
+
+ private void processStreamFeatures(Tag currentTag)
+ throws XmlPullParserException, IOException {
+ this.streamFeatures = tagReader.readElement(currentTag);
+ if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
+ sendStartTLS();
+ } else if (compressionAvailable()) {
+ sendCompressionZlib();
+ } else if (this.streamFeatures.hasChild("register")
+ && account.isOptionSet(Account.OPTION_REGISTER)
+ && usingEncryption) {
+ sendRegistryRequest();
+ } else if (!this.streamFeatures.hasChild("register")
+ && account.isOptionSet(Account.OPTION_REGISTER)) {
+ changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
+ disconnect(true);
+ } else if (this.streamFeatures.hasChild("mechanisms")
+ && shouldAuthenticate && usingEncryption) {
+ List<String> mechanisms = extractMechanisms(streamFeatures
+ .findChild("mechanisms"));
+ if (mechanisms.contains("PLAIN")) {
+ sendSaslAuthPlain();
+ } else if (mechanisms.contains("DIGEST-MD5")) {
+ sendSaslAuthDigestMd5();
+ }
+ } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
+ + smVersion)
+ && streamId != null) {
+ ResumePacket resume = new ResumePacket(this.streamId,
+ stanzasReceived, smVersion);
+ this.tagWriter.writeStanzaAsync(resume);
+ } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
+ sendBindRequest();
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": incompatible server. disconnecting");
+ disconnect(true);
+ }
+ }
+
+ private boolean compressionAvailable() {
+ if (!this.streamFeatures.hasChild("compression",
+ "http://jabber.org/features/compress"))
+ return false;
+ if (!ZLibOutputStream.SUPPORTED)
+ return false;
+ if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
+ return false;
+
+ Element compression = this.streamFeatures.findChild("compression",
+ "http://jabber.org/features/compress");
+ for (Element child : compression.getChildren()) {
+ if (!"method".equals(child.getName()))
+ continue;
+
+ if ("zlib".equalsIgnoreCase(child.getContent())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<String> extractMechanisms(Element stream) {
+ ArrayList<String> mechanisms = new ArrayList<String>(stream
+ .getChildren().size());
+ for (Element child : stream.getChildren()) {
+ mechanisms.add(child.getContent());
+ }
+ return mechanisms;
+ }
+
+ private void sendRegistryRequest() {
+ IqPacket register = new IqPacket(IqPacket.TYPE_GET);
+ register.query("jabber:iq:register");
+ register.setTo(account.getServer());
+ sendIqPacket(register, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element instructions = packet.query().findChild("instructions");
+ if (packet.query().hasChild("username")
+ && (packet.query().hasChild("password"))) {
+ IqPacket register = new IqPacket(IqPacket.TYPE_SET);
+ Element username = new Element("username")
+ .setContent(account.getUsername());
+ Element password = new Element("password")
+ .setContent(account.getPassword());
+ register.query("jabber:iq:register").addChild(username);
+ register.query().addChild(password);
+ sendIqPacket(register, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_RESULT) {
+ account.setOption(Account.OPTION_REGISTER,
+ false);
+ changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
+ } else if (packet.hasChild("error")
+ && (packet.findChild("error")
+ .hasChild("conflict"))) {
+ changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
+ } else {
+ changeStatus(Account.STATUS_REGISTRATION_FAILED);
+ Log.d(Config.LOGTAG, packet.toString());
+ }
+ disconnect(true);
+ }
+ });
+ } else {
+ changeStatus(Account.STATUS_REGISTRATION_FAILED);
+ disconnect(true);
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": could not register. instructions are"
+ + instructions.getContent());
+ }
+ }
+ });
+ }
+
+ private void sendBindRequest() throws IOException {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
+ .addChild("resource").setContent(account.getResource());
+ this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element bind = packet.findChild("bind");
+ if (bind != null) {
+ Element jid = bind.findChild("jid");
+ if (jid != null && jid.getContent() != null) {
+ account.setResource(jid.getContent().split("/", 2)[1]);
+ if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
+ smVersion = 3;
+ EnablePacket enable = new EnablePacket(smVersion);
+ tagWriter.writeStanzaAsync(enable);
+ stanzasSent = 0;
+ messageReceipts.clear();
+ } else if (streamFeatures.hasChild("sm",
+ "urn:xmpp:sm:2")) {
+ smVersion = 2;
+ EnablePacket enable = new EnablePacket(smVersion);
+ tagWriter.writeStanzaAsync(enable);
+ stanzasSent = 0;
+ messageReceipts.clear();
+ }
+ sendServiceDiscoveryInfo(account.getServer());
+ sendServiceDiscoveryItems(account.getServer());
+ if (bindListener != null) {
+ bindListener.onBind(account);
+ }
+ sendInitialPing();
+ } else {
+ disconnect(true);
+ }
+ } else {
+ disconnect(true);
+ }
+ }
+ });
+ if (this.streamFeatures.hasChild("session")) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": sending deprecated session");
+ IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
+ startSession.addChild("session",
+ "urn:ietf:params:xml:ns:xmpp-session");
+ this.sendUnboundIqPacket(startSession, null);
+ }
+ }
+
+ private void sendServiceDiscoveryInfo(final String server) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setTo(server);
+ iq.query("http://jabber.org/protocol/disco#info");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ List<Element> elements = packet.query().getChildren();
+ List<String> features = new ArrayList<String>();
+ for (int i = 0; i < elements.size(); ++i) {
+ if (elements.get(i).getName().equals("feature")) {
+ features.add(elements.get(i).getAttribute("var"));
+ }
+ }
+ disco.put(server, features);
+
+ if (account.getServer().equals(server)) {
+ enableAdvancedStreamFeatures();
+ }
+ }
+ });
+ }
+
+ private void enableAdvancedStreamFeatures() {
+ if (getFeatures().carbons()) {
+ sendEnableCarbons();
+ }
+ }
+
+ private void sendServiceDiscoveryItems(final String server) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setTo(server);
+ iq.query("http://jabber.org/protocol/disco#items");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ List<Element> elements = packet.query().getChildren();
+ for (int i = 0; i < elements.size(); ++i) {
+ if (elements.get(i).getName().equals("item")) {
+ String jid = elements.get(i).getAttribute("jid");
+ sendServiceDiscoveryInfo(jid);
+ }
+ }
+ }
+ });
+ }
+
+ private void sendEnableCarbons() {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.addChild("enable", "urn:xmpp:carbons:2");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (!packet.hasChild("error")) {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": successfully enabled carbons");
+ } else {
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": error enableing carbons " + packet.toString());
+ }
+ }
+ });
+ }
+
+ private void processStreamError(Tag currentTag)
+ throws XmlPullParserException, IOException {
+ Element streamError = tagReader.readElement(currentTag);
+ if (streamError != null && streamError.hasChild("conflict")) {
+ String resource = account.getResource().split("\\.")[0];
+ account.setResource(resource + "." + nextRandomId());
+ Log.d(Config.LOGTAG,
+ account.getJid() + ": switching resource due to conflict ("
+ + account.getResource() + ")");
+ }
+ }
+
+ private void sendStartStream() throws IOException {
+ Tag stream = Tag.start("stream:stream");
+ stream.setAttribute("from", account.getJid());
+ stream.setAttribute("to", account.getServer());
+ stream.setAttribute("version", "1.0");
+ stream.setAttribute("xml:lang", "en");
+ stream.setAttribute("xmlns", "jabber:client");
+ stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
+ tagWriter.writeTag(stream);
+ }
+
+ private String nextRandomId() {
+ return new BigInteger(50, mRandom).toString(32);
+ }
+
+ public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
+ if (packet.getId() == null) {
+ String id = nextRandomId();
+ packet.setAttribute("id", id);
+ }
+ packet.setFrom(account.getFullJid());
+ this.sendPacket(packet, callback);
+ }
+
+ public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
+ if (packet.getId() == null) {
+ String id = nextRandomId();
+ packet.setAttribute("id", id);
+ }
+ this.sendPacket(packet, callback);
+ }
+
+ public void sendMessagePacket(MessagePacket packet) {
+ this.sendPacket(packet, null);
+ }
+
+ public void sendPresencePacket(PresencePacket packet) {
+ this.sendPacket(packet, null);
+ }
+
+ private synchronized void sendPacket(final AbstractStanza packet,
+ PacketReceived callback) {
+ if (packet.getName().equals("iq") || packet.getName().equals("message")
+ || packet.getName().equals("presence")) {
+ ++stanzasSent;
+ }
+ tagWriter.writeStanzaAsync(packet);
+ if (packet instanceof MessagePacket && packet.getId() != null
+ && this.streamId != null) {
+ Log.d(Config.LOGTAG, "request delivery report for stanza "
+ + stanzasSent);
+ this.messageReceipts.put(stanzasSent, packet.getId());
+ tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
+ }
+ if (callback != null) {
+ if (packet.getId() == null) {
+ packet.setId(nextRandomId());
+ }
+ packetCallbacks.put(packet.getId(), callback);
+ }
+ }
+
+ public void sendPing() {
+ if (streamFeatures.hasChild("sm")) {
+ tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+ } else {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setFrom(account.getFullJid());
+ iq.addChild("ping", "urn:xmpp:ping");
+ this.sendIqPacket(iq, null);
+ }
+ this.lastPingSent = SystemClock.elapsedRealtime();
+ }
+
+ public void setOnMessagePacketReceivedListener(
+ OnMessagePacketReceived listener) {
+ this.messageListener = listener;
+ }
+
+ public void setOnUnregisteredIqPacketReceivedListener(
+ OnIqPacketReceived listener) {
+ this.unregisteredIqListener = listener;
+ }
+
+ public void setOnPresencePacketReceivedListener(
+ OnPresencePacketReceived listener) {
+ this.presenceListener = listener;
+ }
+
+ public void setOnJinglePacketReceivedListener(
+ OnJinglePacketReceived listener) {
+ this.jingleListener = listener;
+ }
+
+ public void setOnStatusChangedListener(OnStatusChanged listener) {
+ this.statusListener = listener;
+ }
+
+ public void setOnBindListener(OnBindListener listener) {
+ this.bindListener = listener;
+ }
+
+ public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
+ this.acknowledgedListener = listener;
+ }
+
+ public void disconnect(boolean force) {
+ Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
+ try {
+ if (force) {
+ socket.close();
+ return;
+ }
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (tagWriter.isActive()) {
+ tagWriter.finish();
+ try {
+ while (!tagWriter.finished()) {
+ Log.d(Config.LOGTAG, "not yet finished");
+ Thread.sleep(100);
+ }
+ tagWriter.writeTag(Tag.end("stream:stream"));
+ socket.close();
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG,
+ "io exception during disconnect");
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG, "interrupted");
+ }
+ }
+ }
+ }).start();
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "io exception during disconnect");
+ }
+ }
+
+ public List<String> findDiscoItemsByFeature(String feature) {
+ List<String> items = new ArrayList<String>();
+ for (Entry<String, List<String>> cursor : disco.entrySet()) {
+ if (cursor.getValue().contains(feature)) {
+ items.add(cursor.getKey());
+ }
+ }
+ return items;
+ }
+
+ public String findDiscoItemByFeature(String feature) {
+ List<String> items = findDiscoItemsByFeature(feature);
+ if (items.size() >= 1) {
+ return items.get(0);
+ }
+ return null;
+ }
+
+ public void r() {
+ this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+ }
+
+ public String getMucServer() {
+ return findDiscoItemByFeature("http://jabber.org/protocol/muc");
+ }
+
+ public int getTimeToNextAttempt() {
+ int interval = (int) (25 * Math.pow(1.5, attempt));
+ int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
+ return interval - secondsSinceLast;
+ }
+
+ public int getAttempt() {
+ return this.attempt;
+ }
+
+ public Features getFeatures() {
+ return this.features;
+ }
+
+ public class Features {
+ XmppConnection connection;
+
+ public Features(XmppConnection connection) {
+ this.connection = connection;
+ }
+
+ private boolean hasDiscoFeature(String server, String feature) {
+ if (!connection.disco.containsKey(server)) {
+ return false;
+ }
+ return connection.disco.get(server).contains(feature);
+ }
+
+ public boolean carbons() {
+ return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
+ }
+
+ public boolean sm() {
+ return streamId != null;
+ }
+
+ public boolean csi() {
+ if (connection.streamFeatures == null) {
+ return false;
+ } else {
+ return connection.streamFeatures.hasChild("csi",
+ "urn:xmpp:csi:0");
+ }
+ }
+
+ public boolean pubsub() {
+ return hasDiscoFeature(account.getServer(),
+ "http://jabber.org/protocol/pubsub#publish");
+ }
+
+ public boolean rosterVersioning() {
+ if (connection.streamFeatures == null) {
+ return false;
+ } else {
+ return connection.streamFeatures.hasChild("ver");
+ }
+ }
+
+ public boolean streamhost() {
+ return connection
+ .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
+ }
+
+ public boolean compression() {
+ return connection.usingCompression;
+ }
+ }
+
+ public long getLastSessionEstablished() {
+ long diff;
+ if (this.lastSessionStarted == 0) {
+ diff = SystemClock.elapsedRealtime() - this.lastConnect;
+ } else {
+ diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
+ }
+ return System.currentTimeMillis() - diff;
+ }
+
+ public long getLastConnect() {
+ return this.lastConnect;
+ }
+
+ public long getLastPingSent() {
+ return this.lastPingSent;
+ }
+
+ public long getLastPacketReceived() {
+ return this.lastPaketReceived;
+ }
+
+ public void sendActive() {
+ this.sendPacket(new ActivePacket(), null);
+ }
+
+ public void sendInactive() {
+ this.sendPacket(new InactivePacket(), null);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
new file mode 100644
index 00000000..3e7c7b68
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
@@ -0,0 +1,143 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+public class JingleCandidate {
+
+ public static int TYPE_UNKNOWN;
+ public static int TYPE_DIRECT = 0;
+ public static int TYPE_PROXY = 1;
+
+ private boolean ours;
+ private boolean usedByCounterpart = false;
+ private String cid;
+ private String host;
+ private int port;
+ private int type;
+ private String jid;
+ private int priority;
+
+ public JingleCandidate(String cid, boolean ours) {
+ this.ours = ours;
+ this.cid = cid;
+ }
+
+ public String getCid() {
+ return cid;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public String getHost() {
+ return this.host;
+ }
+
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ public String getJid() {
+ return this.jid;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public void setType(String type) {
+ if ("proxy".equals(type)) {
+ this.type = TYPE_PROXY;
+ } else if ("direct".equals(type)) {
+ this.type = TYPE_DIRECT;
+ } else {
+ this.type = TYPE_UNKNOWN;
+ }
+ }
+
+ public void setPriority(int i) {
+ this.priority = i;
+ }
+
+ public int getPriority() {
+ return this.priority;
+ }
+
+ public boolean equals(JingleCandidate other) {
+ return this.getCid().equals(other.getCid());
+ }
+
+ public boolean equalValues(JingleCandidate other) {
+ return other.getHost().equals(this.getHost())
+ && (other.getPort() == this.getPort());
+ }
+
+ public boolean isOurs() {
+ return ours;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public static List<JingleCandidate> parse(List<Element> canditates) {
+ List<JingleCandidate> parsedCandidates = new ArrayList<JingleCandidate>();
+ for (Element c : canditates) {
+ parsedCandidates.add(JingleCandidate.parse(c));
+ }
+ return parsedCandidates;
+ }
+
+ public static JingleCandidate parse(Element candidate) {
+ JingleCandidate parsedCandidate = new JingleCandidate(
+ candidate.getAttribute("cid"), false);
+ parsedCandidate.setHost(candidate.getAttribute("host"));
+ parsedCandidate.setJid(candidate.getAttribute("jid"));
+ parsedCandidate.setType(candidate.getAttribute("type"));
+ parsedCandidate.setPriority(Integer.parseInt(candidate
+ .getAttribute("priority")));
+ parsedCandidate
+ .setPort(Integer.parseInt(candidate.getAttribute("port")));
+ return parsedCandidate;
+ }
+
+ public Element toElement() {
+ Element element = new Element("candidate");
+ element.setAttribute("cid", this.getCid());
+ element.setAttribute("host", this.getHost());
+ element.setAttribute("port", Integer.toString(this.getPort()));
+ element.setAttribute("jid", this.getJid());
+ element.setAttribute("priority", Integer.toString(this.getPriority()));
+ if (this.getType() == TYPE_DIRECT) {
+ element.setAttribute("type", "direct");
+ } else if (this.getType() == TYPE_PROXY) {
+ element.setAttribute("type", "proxy");
+ }
+ return element;
+ }
+
+ public void flagAsUsedByCounterpart() {
+ this.usedByCounterpart = true;
+ }
+
+ public boolean isUsedByCounterpart() {
+ return this.usedByCounterpart;
+ }
+
+ public String toString() {
+ return this.getHost() + ":" + this.getPort() + " (prio="
+ + this.getPriority() + ")";
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
new file mode 100644
index 00000000..a0b2feb2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -0,0 +1,910 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnection implements Downloadable {
+
+ private final String[] extensions = { "webp", "jpeg", "jpg", "png" };
+ private final String[] cryptoExtensions = { "pgp", "gpg", "otr" };
+
+ private JingleConnectionManager mJingleConnectionManager;
+ private XmppConnectionService mXmppConnectionService;
+
+ protected static final int JINGLE_STATUS_INITIATED = 0;
+ protected static final int JINGLE_STATUS_ACCEPTED = 1;
+ protected static final int JINGLE_STATUS_TERMINATED = 2;
+ protected static final int JINGLE_STATUS_CANCELED = 3;
+ protected static final int JINGLE_STATUS_FINISHED = 4;
+ protected static final int JINGLE_STATUS_TRANSMITTING = 5;
+ protected static final int JINGLE_STATUS_FAILED = 99;
+
+ private int ibbBlockSize = 4096;
+
+ private int mJingleStatus = -1;
+ private int mStatus = -1;
+ private Message message;
+ private String sessionId;
+ private Account account;
+ private String initiator;
+ private String responder;
+ private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
+ private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<String, JingleSocks5Transport>();
+
+ private String transportId;
+ private Element fileOffer;
+ private DownloadableFile file = null;
+
+ private String contentName;
+ private String contentCreator;
+
+ private boolean receivedCandidate = false;
+ private boolean sentCandidate = false;
+
+ private boolean acceptedAutomatically = false;
+
+ private JingleTransport transport = null;
+
+ private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_ERROR) {
+ if (initiator.equals(account.getFullJid())) {
+ mXmppConnectionService.markMessage(message,
+ Message.STATUS_SEND_FAILED);
+ }
+ mJingleStatus = JINGLE_STATUS_FAILED;
+ }
+ }
+ };
+
+ final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
+
+ @Override
+ public void onFileTransmitted(DownloadableFile file) {
+ if (responder.equals(account.getFullJid())) {
+ sendSuccess();
+ if (acceptedAutomatically) {
+ message.markUnread();
+ JingleConnection.this.mXmppConnectionService
+ .getNotificationService().push(message);
+ }
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ message.setBody(Long.toString(file.getSize()) + ','
+ + imageWidth + ',' + imageHeight);
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ mXmppConnectionService.markMessage(message,
+ Message.STATUS_RECEIVED);
+ }
+ Log.d(Config.LOGTAG,
+ "sucessfully transmitted file:" + file.getAbsolutePath());
+ if (message.getEncryption() != Message.ENCRYPTION_PGP) {
+ Intent intent = new Intent(
+ Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ mXmppConnectionService.sendBroadcast(intent);
+ }
+ }
+
+ @Override
+ public void onFileTransferAborted() {
+ JingleConnection.this.sendCancel();
+ JingleConnection.this.cancel();
+ }
+ };
+
+ private OnProxyActivated onProxyActivated = new OnProxyActivated() {
+
+ @Override
+ public void success() {
+ if (initiator.equals(account.getFullJid())) {
+ Log.d(Config.LOGTAG, "we were initiating. sending file");
+ transport.send(file, onFileTransmissionSatusChanged);
+ } else {
+ transport.receive(file, onFileTransmissionSatusChanged);
+ Log.d(Config.LOGTAG, "we were responding. receiving file");
+ }
+ }
+
+ @Override
+ public void failed() {
+ Log.d(Config.LOGTAG, "proxy activation failed");
+ }
+ };
+
+ public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
+ this.mJingleConnectionManager = mJingleConnectionManager;
+ this.mXmppConnectionService = mJingleConnectionManager
+ .getXmppConnectionService();
+ }
+
+ public String getSessionId() {
+ return this.sessionId;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public String getCounterPart() {
+ return this.message.getCounterpart();
+ }
+
+ public void deliverPacket(JinglePacket packet) {
+ boolean returnResult = true;
+ if (packet.isAction("session-terminate")) {
+ Reason reason = packet.getReason();
+ if (reason != null) {
+ if (reason.hasChild("cancel")) {
+ this.cancel();
+ } else if (reason.hasChild("success")) {
+ this.receiveSuccess();
+ } else {
+ this.cancel();
+ }
+ } else {
+ this.cancel();
+ }
+ } else if (packet.isAction("session-accept")) {
+ returnResult = receiveAccept(packet);
+ } else if (packet.isAction("transport-info")) {
+ returnResult = receiveTransportInfo(packet);
+ } else if (packet.isAction("transport-replace")) {
+ if (packet.getJingleContent().hasIbbTransport()) {
+ returnResult = this.receiveFallbackToIbb(packet);
+ } else {
+ returnResult = false;
+ Log.d(Config.LOGTAG, "trying to fallback to something unknown"
+ + packet.toString());
+ }
+ } else if (packet.isAction("transport-accept")) {
+ returnResult = this.receiveTransportAccept(packet);
+ } else {
+ Log.d(Config.LOGTAG, "packet arrived in connection. action was "
+ + packet.getAction());
+ returnResult = false;
+ }
+ IqPacket response;
+ if (returnResult) {
+ response = packet.generateRespone(IqPacket.TYPE_RESULT);
+
+ } else {
+ response = packet.generateRespone(IqPacket.TYPE_ERROR);
+ }
+ account.getXmppConnection().sendIqPacket(response, null);
+ }
+
+ public void init(Message message) {
+ this.contentCreator = "initiator";
+ this.contentName = this.mJingleConnectionManager.nextRandomId();
+ this.message = message;
+ this.account = message.getConversation().getAccount();
+ this.initiator = this.account.getFullJid();
+ this.responder = this.message.getCounterpart();
+ this.sessionId = this.mJingleConnectionManager.nextRandomId();
+ if (this.candidates.size() > 0) {
+ this.sendInitRequest();
+ } else {
+ this.mJingleConnectionManager.getPrimaryCandidate(account,
+ new OnPrimaryCandidateFound() {
+
+ @Override
+ public void onPrimaryCandidateFound(boolean success,
+ final JingleCandidate candidate) {
+ if (success) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ JingleConnection.this, candidate);
+ connections.put(candidate.getCid(),
+ socksConnection);
+ socksConnection
+ .connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Log.d(Config.LOGTAG,
+ "connection to our own primary candidete failed");
+ sendInitRequest();
+ }
+
+ @Override
+ public void established() {
+ Log.d(Config.LOGTAG,
+ "succesfully connected to our own primary candidate");
+ mergeCandidate(candidate);
+ sendInitRequest();
+ }
+ });
+ mergeCandidate(candidate);
+ } else {
+ Log.d(Config.LOGTAG,
+ "no primary candidate of our own was found");
+ sendInitRequest();
+ }
+ }
+ });
+ }
+
+ }
+
+ public void init(Account account, JinglePacket packet) {
+ this.mJingleStatus = JINGLE_STATUS_INITIATED;
+ Conversation conversation = this.mXmppConnectionService
+ .findOrCreateConversation(account,
+ packet.getFrom().split("/", 2)[0], false);
+ this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setType(Message.TYPE_IMAGE);
+ this.mStatus = Downloadable.STATUS_OFFER;
+ this.message.setDownloadable(this);
+ String[] fromParts = packet.getFrom().split("/", 2);
+ this.message.setPresence(fromParts[1]);
+ this.account = account;
+ this.initiator = packet.getFrom();
+ this.responder = this.account.getFullJid();
+ this.sessionId = packet.getSessionId();
+ Content content = packet.getJingleContent();
+ this.contentCreator = content.getAttribute("creator");
+ this.contentName = content.getAttribute("name");
+ this.transportId = content.getTransportId();
+ this.mergeCandidates(JingleCandidate.parse(content.socks5transport()
+ .getChildren()));
+ this.fileOffer = packet.getJingleContent().getFileOffer();
+ if (fileOffer != null) {
+ Element fileSize = fileOffer.findChild("size");
+ Element fileNameElement = fileOffer.findChild("name");
+ if (fileNameElement != null) {
+ boolean supportedFile = false;
+ String[] filename = fileNameElement.getContent()
+ .toLowerCase(Locale.US).split("\\.");
+ if (Arrays.asList(this.extensions).contains(
+ filename[filename.length - 1])) {
+ supportedFile = true;
+ } else if (Arrays.asList(this.cryptoExtensions).contains(
+ filename[filename.length - 1])) {
+ if (filename.length == 3) {
+ if (Arrays.asList(this.extensions).contains(
+ filename[filename.length - 2])) {
+ supportedFile = true;
+ if (filename[filename.length - 1].equals("otr")) {
+ Log.d(Config.LOGTAG, "receiving otr file");
+ this.message
+ .setEncryption(Message.ENCRYPTION_OTR);
+ } else {
+ this.message
+ .setEncryption(Message.ENCRYPTION_PGP);
+ }
+ }
+ }
+ }
+ if (supportedFile) {
+ long size = Long.parseLong(fileSize.getContent());
+ message.setBody(Long.toString(size));
+ conversation.add(message);
+ mXmppConnectionService.updateConversationUi();
+ if (size <= this.mJingleConnectionManager
+ .getAutoAcceptFileSize()) {
+ Log.d(Config.LOGTAG, "auto accepting file from "
+ + packet.getFrom());
+ this.acceptedAutomatically = true;
+ this.sendAccept();
+ } else {
+ message.markUnread();
+ Log.d(Config.LOGTAG,
+ "not auto accepting new file offer with size: "
+ + size
+ + " allowed size:"
+ + this.mJingleConnectionManager
+ .getAutoAcceptFileSize());
+ this.mXmppConnectionService.getNotificationService()
+ .push(message);
+ }
+ this.file = this.mXmppConnectionService.getFileBackend()
+ .getFile(message, false);
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ byte[] key = conversation.getSymmetricKey();
+ if (key == null) {
+ this.sendCancel();
+ this.cancel();
+ return;
+ } else {
+ this.file.setKey(key);
+ }
+ }
+ this.file.setExpectedSize(size);
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ }
+
+ private void sendInitRequest() {
+ JinglePacket packet = this.bootstrapPacket("session-initiate");
+ Content content = new Content(this.contentCreator, this.contentName);
+ if (message.getType() == Message.TYPE_IMAGE) {
+ content.setTransportId(this.transportId);
+ this.file = this.mXmppConnectionService.getFileBackend().getFile(
+ message, false);
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Conversation conversation = this.message.getConversation();
+ this.mXmppConnectionService.renewSymmetricKey(conversation);
+ content.setFileOffer(this.file, true);
+ this.file.setKey(conversation.getSymmetricKey());
+ } else {
+ content.setFileOffer(this.file, false);
+ }
+ this.transportId = this.mJingleConnectionManager.nextRandomId();
+ content.setTransportId(this.transportId);
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ this.mJingleStatus = JINGLE_STATUS_INITIATED;
+ }
+ }
+
+ private List<Element> getCandidatesAsElements() {
+ List<Element> elements = new ArrayList<Element>();
+ for (JingleCandidate c : this.candidates) {
+ elements.add(c.toElement());
+ }
+ return elements;
+ }
+
+ private void sendAccept() {
+ mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ this.mStatus = Downloadable.STATUS_DOWNLOADING;
+ mXmppConnectionService.updateConversationUi();
+ this.mJingleConnectionManager.getPrimaryCandidate(this.account,
+ new OnPrimaryCandidateFound() {
+
+ @Override
+ public void onPrimaryCandidateFound(boolean success,
+ final JingleCandidate candidate) {
+ final JinglePacket packet = bootstrapPacket("session-accept");
+ final Content content = new Content(contentCreator,
+ contentName);
+ content.setFileOffer(fileOffer);
+ content.setTransportId(transportId);
+ if ((success) && (!equalCandidateExists(candidate))) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ JingleConnection.this, candidate);
+ connections.put(candidate.getCid(), socksConnection);
+ socksConnection.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Log.d(Config.LOGTAG,
+ "connection to our own primary candidate failed");
+ content.socks5transport().setChildren(
+ getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Log.d(Config.LOGTAG,
+ "connected to primary candidate");
+ mergeCandidate(candidate);
+ content.socks5transport().setChildren(
+ getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+ });
+ } else {
+ Log.d(Config.LOGTAG,
+ "did not find a primary candidate for ourself");
+ content.socks5transport().setChildren(
+ getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+ }
+ });
+
+ }
+
+ private JinglePacket bootstrapPacket(String action) {
+ JinglePacket packet = new JinglePacket();
+ packet.setAction(action);
+ packet.setFrom(account.getFullJid());
+ packet.setTo(this.message.getCounterpart());
+ packet.setSessionId(this.sessionId);
+ packet.setInitiator(this.initiator);
+ return packet;
+ }
+
+ private void sendJinglePacket(JinglePacket packet) {
+ // Log.d(Config.LOGTAG,packet.toString());
+ account.getXmppConnection().sendIqPacket(packet, responseListener);
+ }
+
+ private boolean receiveAccept(JinglePacket packet) {
+ Content content = packet.getJingleContent();
+ mergeCandidates(JingleCandidate.parse(content.socks5transport()
+ .getChildren()));
+ this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
+ this.connectNextCandidate();
+ return true;
+ }
+
+ private boolean receiveTransportInfo(JinglePacket packet) {
+ Content content = packet.getJingleContent();
+ if (content.hasSocks5Transport()) {
+ if (content.socks5transport().hasChild("activated")) {
+ if ((this.transport != null)
+ && (this.transport instanceof JingleSocks5Transport)) {
+ onProxyActivated.success();
+ } else {
+ String cid = content.socks5transport()
+ .findChild("activated").getAttribute("cid");
+ Log.d(Config.LOGTAG, "received proxy activated (" + cid
+ + ")prior to choosing our own transport");
+ JingleSocks5Transport connection = this.connections
+ .get(cid);
+ if (connection != null) {
+ connection.setActivated(true);
+ } else {
+ Log.d(Config.LOGTAG, "activated connection not found");
+ this.sendCancel();
+ this.cancel();
+ }
+ }
+ return true;
+ } else if (content.socks5transport().hasChild("proxy-error")) {
+ onProxyActivated.failed();
+ return true;
+ } else if (content.socks5transport().hasChild("candidate-error")) {
+ Log.d(Config.LOGTAG, "received candidate error");
+ this.receivedCandidate = true;
+ if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+ && (this.sentCandidate)) {
+ this.connect();
+ }
+ return true;
+ } else if (content.socks5transport().hasChild("candidate-used")) {
+ String cid = content.socks5transport()
+ .findChild("candidate-used").getAttribute("cid");
+ if (cid != null) {
+ Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
+ JingleCandidate candidate = getCandidate(cid);
+ candidate.flagAsUsedByCounterpart();
+ this.receivedCandidate = true;
+ if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+ && (this.sentCandidate)) {
+ this.connect();
+ } else {
+ Log.d(Config.LOGTAG,
+ "ignoring because file is already in transmission or we havent sent our candidate yet");
+ }
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ private void connect() {
+ final JingleSocks5Transport connection = chooseConnection();
+ this.transport = connection;
+ if (connection == null) {
+ Log.d(Config.LOGTAG, "could not find suitable candidate");
+ this.disconnect();
+ if (this.initiator.equals(account.getFullJid())) {
+ this.sendFallbackToIbb();
+ }
+ } else {
+ this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
+ if (connection.needsActivation()) {
+ if (connection.getCandidate().isOurs()) {
+ Log.d(Config.LOGTAG, "candidate "
+ + connection.getCandidate().getCid()
+ + " was our proxy. going to activate");
+ IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
+ activation.setTo(connection.getCandidate().getJid());
+ activation.query("http://jabber.org/protocol/bytestreams")
+ .setAttribute("sid", this.getSessionId());
+ activation.query().addChild("activate")
+ .setContent(this.getCounterPart());
+ this.account.getXmppConnection().sendIqPacket(activation,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_ERROR) {
+ onProxyActivated.failed();
+ } else {
+ onProxyActivated.success();
+ sendProxyActivated(connection
+ .getCandidate().getCid());
+ }
+ }
+ });
+ } else {
+ Log.d(Config.LOGTAG,
+ "candidate "
+ + connection.getCandidate().getCid()
+ + " was a proxy. waiting for other party to activate");
+ }
+ } else {
+ if (initiator.equals(account.getFullJid())) {
+ Log.d(Config.LOGTAG, "we were initiating. sending file");
+ connection.send(file, onFileTransmissionSatusChanged);
+ } else {
+ Log.d(Config.LOGTAG, "we were responding. receiving file");
+ connection.receive(file, onFileTransmissionSatusChanged);
+ }
+ }
+ }
+ }
+
+ private JingleSocks5Transport chooseConnection() {
+ JingleSocks5Transport connection = null;
+ for (Entry<String, JingleSocks5Transport> cursor : connections
+ .entrySet()) {
+ JingleSocks5Transport currentConnection = cursor.getValue();
+ // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
+ if (currentConnection.isEstablished()
+ && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
+ .getCandidate().isOurs()))) {
+ // Log.d(Config.LOGTAG,"is usable");
+ if (connection == null) {
+ connection = currentConnection;
+ } else {
+ if (connection.getCandidate().getPriority() < currentConnection
+ .getCandidate().getPriority()) {
+ connection = currentConnection;
+ } else if (connection.getCandidate().getPriority() == currentConnection
+ .getCandidate().getPriority()) {
+ // Log.d(Config.LOGTAG,"found two candidates with same priority");
+ if (initiator.equals(account.getFullJid())) {
+ if (currentConnection.getCandidate().isOurs()) {
+ connection = currentConnection;
+ }
+ } else {
+ if (!currentConnection.getCandidate().isOurs()) {
+ connection = currentConnection;
+ }
+ }
+ }
+ }
+ }
+ }
+ return connection;
+ }
+
+ private void sendSuccess() {
+ JinglePacket packet = bootstrapPacket("session-terminate");
+ Reason reason = new Reason();
+ reason.addChild("success");
+ packet.setReason(reason);
+ this.sendJinglePacket(packet);
+ this.disconnect();
+ this.mJingleStatus = JINGLE_STATUS_FINISHED;
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setDownloadable(null);
+ this.mXmppConnectionService.updateMessage(message);
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ private void sendFallbackToIbb() {
+ Log.d(Config.LOGTAG, "sending fallback to ibb");
+ JinglePacket packet = this.bootstrapPacket("transport-replace");
+ Content content = new Content(this.contentCreator, this.contentName);
+ this.transportId = this.mJingleConnectionManager.nextRandomId();
+ content.setTransportId(this.transportId);
+ content.ibbTransport().setAttribute("block-size",
+ Integer.toString(this.ibbBlockSize));
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ }
+
+ private boolean receiveFallbackToIbb(JinglePacket packet) {
+ Log.d(Config.LOGTAG, "receiving fallack to ibb");
+ String receivedBlockSize = packet.getJingleContent().ibbTransport()
+ .getAttribute("block-size");
+ if (receivedBlockSize != null) {
+ int bs = Integer.parseInt(receivedBlockSize);
+ if (bs > this.ibbBlockSize) {
+ this.ibbBlockSize = bs;
+ }
+ }
+ this.transportId = packet.getJingleContent().getTransportId();
+ this.transport = new JingleInbandTransport(this.account,
+ this.responder, this.transportId, this.ibbBlockSize);
+ this.transport.receive(file, onFileTransmissionSatusChanged);
+ JinglePacket answer = bootstrapPacket("transport-accept");
+ Content content = new Content("initiator", "a-file-offer");
+ content.setTransportId(this.transportId);
+ content.ibbTransport().setAttribute("block-size",
+ Integer.toString(this.ibbBlockSize));
+ answer.setContent(content);
+ this.sendJinglePacket(answer);
+ return true;
+ }
+
+ private boolean receiveTransportAccept(JinglePacket packet) {
+ if (packet.getJingleContent().hasIbbTransport()) {
+ String receivedBlockSize = packet.getJingleContent().ibbTransport()
+ .getAttribute("block-size");
+ if (receivedBlockSize != null) {
+ int bs = Integer.parseInt(receivedBlockSize);
+ if (bs > this.ibbBlockSize) {
+ this.ibbBlockSize = bs;
+ }
+ }
+ this.transport = new JingleInbandTransport(this.account,
+ this.responder, this.transportId, this.ibbBlockSize);
+ this.transport.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Log.d(Config.LOGTAG, "ibb open failed");
+ }
+
+ @Override
+ public void established() {
+ JingleConnection.this.transport.send(file,
+ onFileTransmissionSatusChanged);
+ }
+ });
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void receiveSuccess() {
+ this.mJingleStatus = JINGLE_STATUS_FINISHED;
+ this.mXmppConnectionService.markMessage(this.message,
+ Message.STATUS_SEND);
+ this.disconnect();
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ public void cancel() {
+ this.mJingleStatus = JINGLE_STATUS_CANCELED;
+ this.disconnect();
+ if (this.message != null) {
+ if (this.responder.equals(account.getFullJid())) {
+ this.mStatus = Downloadable.STATUS_FAILED;
+ this.mXmppConnectionService.updateConversationUi();
+ } else {
+ if (this.mJingleStatus == JINGLE_STATUS_INITIATED) {
+ this.mXmppConnectionService.markMessage(this.message,
+ Message.STATUS_SEND_REJECTED);
+ } else {
+ this.mXmppConnectionService.markMessage(this.message,
+ Message.STATUS_SEND_FAILED);
+ }
+ }
+ }
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ private void sendCancel() {
+ JinglePacket packet = bootstrapPacket("session-terminate");
+ Reason reason = new Reason();
+ reason.addChild("cancel");
+ packet.setReason(reason);
+ this.sendJinglePacket(packet);
+ }
+
+ private void connectNextCandidate() {
+ for (JingleCandidate candidate : this.candidates) {
+ if ((!connections.containsKey(candidate.getCid()) && (!candidate
+ .isOurs()))) {
+ this.connectWithCandidate(candidate);
+ return;
+ }
+ }
+ this.sendCandidateError();
+ }
+
+ private void connectWithCandidate(final JingleCandidate candidate) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ this, candidate);
+ connections.put(candidate.getCid(), socksConnection);
+ socksConnection.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Log.d(Config.LOGTAG,
+ "connection failed with " + candidate.getHost() + ":"
+ + candidate.getPort());
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Log.d(Config.LOGTAG,
+ "established connection with " + candidate.getHost()
+ + ":" + candidate.getPort());
+ sendCandidateUsed(candidate.getCid());
+ }
+ });
+ }
+
+ private void disconnect() {
+ Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
+ .entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, JingleSocks5Transport> pairs = it.next();
+ pairs.getValue().disconnect();
+ it.remove();
+ }
+ }
+
+ private void sendProxyActivated(String cid) {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("activated")
+ .setAttribute("cid", cid);
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ }
+
+ private void sendCandidateUsed(final String cid) {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("candidate-used")
+ .setAttribute("cid", cid);
+ packet.setContent(content);
+ this.sentCandidate = true;
+ if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+ connect();
+ }
+ this.sendJinglePacket(packet);
+ }
+
+ private void sendCandidateError() {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("candidate-error");
+ packet.setContent(content);
+ this.sentCandidate = true;
+ if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+ connect();
+ }
+ this.sendJinglePacket(packet);
+ }
+
+ public String getInitiator() {
+ return this.initiator;
+ }
+
+ public String getResponder() {
+ return this.responder;
+ }
+
+ public int getJingleStatus() {
+ return this.mJingleStatus;
+ }
+
+ private boolean equalCandidateExists(JingleCandidate candidate) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.equalValues(candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void mergeCandidate(JingleCandidate candidate) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.equals(candidate)) {
+ return;
+ }
+ }
+ this.candidates.add(candidate);
+ }
+
+ private void mergeCandidates(List<JingleCandidate> candidates) {
+ for (JingleCandidate c : candidates) {
+ mergeCandidate(c);
+ }
+ }
+
+ private JingleCandidate getCandidate(String cid) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.getCid().equals(cid)) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ interface OnProxyActivated {
+ public void success();
+
+ public void failed();
+ }
+
+ public boolean hasTransportId(String sid) {
+ return sid.equals(this.transportId);
+ }
+
+ public JingleTransport getTransport() {
+ return this.transport;
+ }
+
+ public boolean start() {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ if (mJingleStatus == JINGLE_STATUS_INITIATED) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ sendAccept();
+ }
+ }).start();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int getStatus() {
+ return this.mStatus;
+ }
+
+ @Override
+ public long getFileSize() {
+ if (this.file != null) {
+ return this.file.getExpectedSize();
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
new file mode 100644
index 00000000..1e7c84d4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -0,0 +1,163 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import android.annotation.SuppressLint;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnectionManager extends AbstractConnectionManager {
+ private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>();
+
+ private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>();
+
+ @SuppressLint("TrulyRandom")
+ private SecureRandom random = new SecureRandom();
+
+ public JingleConnectionManager(XmppConnectionService service) {
+ super(service);
+ }
+
+ public void deliverPacket(Account account, JinglePacket packet) {
+ if (packet.isAction("session-initiate")) {
+ JingleConnection connection = new JingleConnection(this);
+ connection.init(account, packet);
+ connections.add(connection);
+ } else {
+ for (JingleConnection connection : connections) {
+ if (connection.getAccount() == account
+ && connection.getSessionId().equals(
+ packet.getSessionId())
+ && connection.getCounterPart().equals(packet.getFrom())) {
+ connection.deliverPacket(packet);
+ return;
+ }
+ }
+ account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_ERROR), null);
+ }
+ }
+
+ public JingleConnection createNewConnection(Message message) {
+ JingleConnection connection = new JingleConnection(this);
+ connection.init(message);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public JingleConnection createNewConnection(JinglePacket packet) {
+ JingleConnection connection = new JingleConnection(this);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public void finishConnection(JingleConnection connection) {
+ this.connections.remove(connection);
+ }
+
+ public void getPrimaryCandidate(Account account,
+ final OnPrimaryCandidateFound listener) {
+ if (!this.primaryCandidates.containsKey(account.getJid())) {
+ String xmlns = "http://jabber.org/protocol/bytestreams";
+ final String proxy = account.getXmppConnection()
+ .findDiscoItemByFeature(xmlns);
+ if (proxy != null) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setTo(proxy);
+ iq.query(xmlns);
+ account.getXmppConnection().sendIqPacket(iq,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ Element streamhost = packet
+ .query()
+ .findChild("streamhost",
+ "http://jabber.org/protocol/bytestreams");
+ if (streamhost != null) {
+ JingleCandidate candidate = new JingleCandidate(
+ nextRandomId(), true);
+ candidate.setHost(streamhost
+ .getAttribute("host"));
+ candidate.setPort(Integer
+ .parseInt(streamhost
+ .getAttribute("port")));
+ candidate
+ .setType(JingleCandidate.TYPE_PROXY);
+ candidate.setJid(proxy);
+ candidate.setPriority(655360 + 65535);
+ primaryCandidates.put(account.getJid(),
+ candidate);
+ listener.onPrimaryCandidateFound(true,
+ candidate);
+ } else {
+ listener.onPrimaryCandidateFound(false,
+ null);
+ }
+ }
+ });
+ } else {
+ listener.onPrimaryCandidateFound(false, null);
+ }
+
+ } else {
+ listener.onPrimaryCandidateFound(true,
+ this.primaryCandidates.get(account.getJid()));
+ }
+ }
+
+ public String nextRandomId() {
+ return new BigInteger(50, random).toString(32);
+ }
+
+ public void deliverIbbPacket(Account account, IqPacket packet) {
+ String sid = null;
+ Element payload = null;
+ if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) {
+ payload = packet
+ .findChild("open", "http://jabber.org/protocol/ibb");
+ sid = payload.getAttribute("sid");
+ } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
+ payload = packet
+ .findChild("data", "http://jabber.org/protocol/ibb");
+ sid = payload.getAttribute("sid");
+ }
+ if (sid != null) {
+ for (JingleConnection connection : connections) {
+ if (connection.getAccount() == account
+ && connection.hasTransportId(sid)) {
+ JingleTransport transport = connection.getTransport();
+ if (transport instanceof JingleInbandTransport) {
+ JingleInbandTransport inbandTransport = (JingleInbandTransport) transport;
+ inbandTransport.deliverPayload(packet, payload);
+ return;
+ }
+ }
+ }
+ Log.d(Config.LOGTAG,
+ "couldnt deliver payload: " + payload.toString());
+ } else {
+ Log.d(Config.LOGTAG, "no sid found in incomming ibb packet");
+ }
+ }
+
+ public void cancelInTransmission() {
+ for (JingleConnection connection : this.connections) {
+ if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
+ connection.cancel();
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
new file mode 100644
index 00000000..cc1e92f6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
@@ -0,0 +1,191 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import android.util.Base64;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleInbandTransport extends JingleTransport {
+
+ private Account account;
+ private String counterpart;
+ private int blockSize;
+ private int bufferSize;
+ private int seq = 0;
+ private String sessionId;
+
+ private boolean established = false;
+
+ private DownloadableFile file;
+
+ private InputStream fileInputStream = null;
+ private OutputStream fileOutputStream;
+ private long remainingSize;
+ private MessageDigest digest;
+
+ private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
+
+ private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_RESULT) {
+ sendNextBlock();
+ }
+ }
+ };
+
+ public JingleInbandTransport(Account account, String counterpart,
+ String sid, int blocksize) {
+ this.account = account;
+ this.counterpart = counterpart;
+ this.blockSize = blocksize;
+ this.bufferSize = blocksize / 4;
+ this.sessionId = sid;
+ }
+
+ public void connect(final OnTransportConnected callback) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.setTo(this.counterpart);
+ Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
+ open.setAttribute("sid", this.sessionId);
+ open.setAttribute("stanza", "iq");
+ open.setAttribute("block-size", Integer.toString(this.blockSize));
+
+ this.account.getXmppConnection().sendIqPacket(iq,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_ERROR) {
+ callback.failed();
+ } else {
+ callback.established();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void receive(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ this.fileOutputStream = file.createOutputStream();
+ if (this.fileOutputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ this.remainingSize = file.getExpectedSize();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+
+ @Override
+ public void send(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.digest = MessageDigest.getInstance("SHA-1");
+ this.digest.reset();
+ fileInputStream = this.file.createInputStream();
+ if (fileInputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ this.sendNextBlock();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+
+ private void sendNextBlock() {
+ byte[] buffer = new byte[this.bufferSize];
+ try {
+ int count = fileInputStream.read(buffer);
+ if (count == -1) {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ fileInputStream.close();
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ } else {
+ this.digest.update(buffer);
+ String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP);
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ iq.setTo(this.counterpart);
+ Element data = iq.addChild("data",
+ "http://jabber.org/protocol/ibb");
+ data.setAttribute("seq", Integer.toString(this.seq));
+ data.setAttribute("block-size",
+ Integer.toString(this.blockSize));
+ data.setAttribute("sid", this.sessionId);
+ data.setContent(base64);
+ this.account.getXmppConnection().sendIqPacket(iq,
+ this.onAckReceived);
+ this.seq++;
+ }
+ } catch (IOException e) {
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ private void receiveNextBlock(String data) {
+ try {
+ byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
+ if (this.remainingSize < buffer.length) {
+ buffer = Arrays
+ .copyOfRange(buffer, 0, (int) this.remainingSize);
+ }
+ this.remainingSize -= buffer.length;
+
+ this.fileOutputStream.write(buffer);
+
+ this.digest.update(buffer);
+ if (this.remainingSize <= 0) {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ }
+ } catch (IOException e) {
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ public void deliverPayload(IqPacket packet, Element payload) {
+ if (payload.getName().equals("open")) {
+ if (!established) {
+ established = true;
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_RESULT), null);
+ } else {
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_ERROR), null);
+ }
+ } else if (payload.getName().equals("data")) {
+ this.receiveNextBlock(payload.getContent());
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_RESULT), null);
+ } else {
+ // TODO some sort of exception
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
new file mode 100644
index 00000000..1da2f0cd
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
@@ -0,0 +1,212 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class JingleSocks5Transport extends JingleTransport {
+ private JingleCandidate candidate;
+ private String destination;
+ private OutputStream outputStream;
+ private InputStream inputStream;
+ private boolean isEstablished = false;
+ private boolean activated = false;
+ protected Socket socket;
+
+ public JingleSocks5Transport(JingleConnection jingleConnection,
+ JingleCandidate candidate) {
+ this.candidate = candidate;
+ try {
+ MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
+ StringBuilder destBuilder = new StringBuilder();
+ destBuilder.append(jingleConnection.getSessionId());
+ if (candidate.isOurs()) {
+ destBuilder.append(jingleConnection.getAccount().getFullJid());
+ destBuilder.append(jingleConnection.getCounterPart());
+ } else {
+ destBuilder.append(jingleConnection.getCounterPart());
+ destBuilder.append(jingleConnection.getAccount().getFullJid());
+ }
+ mDigest.reset();
+ this.destination = CryptoHelper.bytesToHex(mDigest
+ .digest(destBuilder.toString().getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+
+ }
+ }
+
+ public void connect(final OnTransportConnected callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ socket = new Socket(candidate.getHost(),
+ candidate.getPort());
+ inputStream = socket.getInputStream();
+ outputStream = socket.getOutputStream();
+ byte[] login = { 0x05, 0x01, 0x00 };
+ byte[] expectedReply = { 0x05, 0x00 };
+ byte[] reply = new byte[2];
+ outputStream.write(login);
+ inputStream.read(reply);
+ final String connect = Character.toString('\u0005')
+ + '\u0001' + '\u0000' + '\u0003' + '\u0028'
+ + destination + '\u0000' + '\u0000';
+ if (Arrays.equals(reply, expectedReply)) {
+ outputStream.write(connect.getBytes());
+ byte[] result = new byte[2];
+ inputStream.read(result);
+ int status = result[1];
+ if (status == 0) {
+ isEstablished = true;
+ callback.established();
+ } else {
+ callback.failed();
+ }
+ } else {
+ socket.close();
+ callback.failed();
+ }
+ } catch (UnknownHostException e) {
+ callback.failed();
+ } catch (IOException e) {
+ callback.failed();
+ }
+ }
+ }).start();
+
+ }
+
+ public void send(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ InputStream fileInputStream = null;
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ fileInputStream = file.createInputStream();
+ if (fileInputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ int count;
+ byte[] buffer = new byte[8192];
+ while ((count = fileInputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ }
+ outputStream.flush();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ if (callback != null) {
+ callback.onFileTransmitted(file);
+ }
+ } catch (FileNotFoundException e) {
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ } finally {
+ try {
+ if (fileInputStream != null) {
+ fileInputStream.close();
+ }
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+ }
+ }).start();
+
+ }
+
+ public void receive(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ inputStream.skip(45);
+ socket.setSoTimeout(30000);
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ OutputStream fileOutputStream = file.createOutputStream();
+ if (fileOutputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ long remainingSize = file.getExpectedSize();
+ byte[] buffer = new byte[8192];
+ int count = buffer.length;
+ while (remainingSize > 0) {
+ count = inputStream.read(buffer);
+ if (count == -1) {
+ callback.onFileTransferAborted();
+ return;
+ } else {
+ fileOutputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ remainingSize -= count;
+ }
+ }
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ callback.onFileTransmitted(file);
+ } catch (FileNotFoundException e) {
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+ }).start();
+ }
+
+ public boolean isProxy() {
+ return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
+ }
+
+ public boolean needsActivation() {
+ return (this.isProxy() && !this.activated);
+ }
+
+ public void disconnect() {
+ if (this.socket != null) {
+ try {
+ this.socket.close();
+ } catch (IOException e) {
+
+ }
+ }
+ }
+
+ public boolean isEstablished() {
+ return this.isEstablished;
+ }
+
+ public JingleCandidate getCandidate() {
+ return this.candidate;
+ }
+
+ public void setActivated(boolean activated) {
+ this.activated = activated;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
new file mode 100644
index 00000000..1374e61c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public abstract class JingleTransport {
+ public abstract void connect(final OnTransportConnected callback);
+
+ public abstract void receive(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+
+ public abstract void send(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
new file mode 100644
index 00000000..e45e7441
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public interface OnFileTransmissionStatusChanged {
+ public void onFileTransmitted(DownloadableFile file);
+
+ public void onFileTransferAborted();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
new file mode 100644
index 00000000..2aaf62a1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.PacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+
+public interface OnJinglePacketReceived extends PacketReceived {
+ public void onJinglePacketReceived(Account account, JinglePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
new file mode 100644
index 00000000..03a437b2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
@@ -0,0 +1,6 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnPrimaryCandidateFound {
+ public void onPrimaryCandidateFound(boolean success,
+ JingleCandidate canditate);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
new file mode 100644
index 00000000..38f03c5d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnTransportConnected {
+ public void failed();
+
+ public void established();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
new file mode 100644
index 00000000..bcadbe77
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
@@ -0,0 +1,102 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Element;
+
+public class Content extends Element {
+
+ private String transportId;
+
+ private Content(String name) {
+ super(name);
+ }
+
+ public Content() {
+ super("content");
+ }
+
+ public Content(String creator, String name) {
+ super("content");
+ this.setAttribute("creator", creator);
+ this.setAttribute("name", name);
+ }
+
+ public void setTransportId(String sid) {
+ this.transportId = sid;
+ }
+
+ public void setFileOffer(DownloadableFile actualFile, boolean otr) {
+ Element description = this.addChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ Element offer = description.addChild("offer");
+ Element file = offer.addChild("file");
+ file.addChild("size").setContent(Long.toString(actualFile.getSize()));
+ if (otr) {
+ file.addChild("name").setContent(actualFile.getName() + ".otr");
+ } else {
+ file.addChild("name").setContent(actualFile.getName());
+ }
+ }
+
+ public Element getFileOffer() {
+ Element description = this.findChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ if (description == null) {
+ return null;
+ }
+ Element offer = description.findChild("offer");
+ if (offer == null) {
+ return null;
+ }
+ return offer.findChild("file");
+ }
+
+ public void setFileOffer(Element fileOffer) {
+ Element description = this.findChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ if (description == null) {
+ description = this.addChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ }
+ description.addChild(fileOffer);
+ }
+
+ public String getTransportId() {
+ if (hasSocks5Transport()) {
+ this.transportId = socks5transport().getAttribute("sid");
+ } else if (hasIbbTransport()) {
+ this.transportId = ibbTransport().getAttribute("sid");
+ }
+ return this.transportId;
+ }
+
+ public Element socks5transport() {
+ Element transport = this.findChild("transport",
+ "urn:xmpp:jingle:transports:s5b:1");
+ if (transport == null) {
+ transport = this.addChild("transport",
+ "urn:xmpp:jingle:transports:s5b:1");
+ transport.setAttribute("sid", this.transportId);
+ }
+ return transport;
+ }
+
+ public Element ibbTransport() {
+ Element transport = this.findChild("transport",
+ "urn:xmpp:jingle:transports:ibb:1");
+ if (transport == null) {
+ transport = this.addChild("transport",
+ "urn:xmpp:jingle:transports:ibb:1");
+ transport.setAttribute("sid", this.transportId);
+ }
+ return transport;
+ }
+
+ public boolean hasSocks5Transport() {
+ return this.hasChild("transport", "urn:xmpp:jingle:transports:s5b:1");
+ }
+
+ public boolean hasIbbTransport() {
+ return this.hasChild("transport", "urn:xmpp:jingle:transports:ibb:1");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
new file mode 100644
index 00000000..77a73643
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JinglePacket extends IqPacket {
+ Content content = null;
+ Reason reason = null;
+ Element jingle = new Element("jingle");
+
+ @Override
+ public Element addChild(Element child) {
+ if ("jingle".equals(child.getName())) {
+ Element contentElement = child.findChild("content");
+ if (contentElement != null) {
+ this.content = new Content();
+ this.content.setChildren(contentElement.getChildren());
+ this.content.setAttributes(contentElement.getAttributes());
+ }
+ Element reasonElement = child.findChild("reason");
+ if (reasonElement != null) {
+ this.reason = new Reason();
+ this.reason.setChildren(reasonElement.getChildren());
+ this.reason.setAttributes(reasonElement.getAttributes());
+ }
+ this.jingle.setAttributes(child.getAttributes());
+ }
+ return child;
+ }
+
+ public JinglePacket setContent(Content content) {
+ this.content = content;
+ return this;
+ }
+
+ public Content getJingleContent() {
+ if (this.content == null) {
+ this.content = new Content();
+ }
+ return this.content;
+ }
+
+ public JinglePacket setReason(Reason reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ public Reason getReason() {
+ return this.reason;
+ }
+
+ private void build() {
+ this.children.clear();
+ this.jingle.clearChildren();
+ this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1");
+ if (this.content != null) {
+ jingle.addChild(this.content);
+ }
+ if (this.reason != null) {
+ jingle.addChild(this.reason);
+ }
+ this.children.add(jingle);
+ this.setAttribute("type", "set");
+ }
+
+ public String getSessionId() {
+ return this.jingle.getAttribute("sid");
+ }
+
+ public void setSessionId(String sid) {
+ this.jingle.setAttribute("sid", sid);
+ }
+
+ @Override
+ public String toString() {
+ this.build();
+ return super.toString();
+ }
+
+ public void setAction(String action) {
+ this.jingle.setAttribute("action", action);
+ }
+
+ public String getAction() {
+ return this.jingle.getAttribute("action");
+ }
+
+ public void setInitiator(String initiator) {
+ this.jingle.setAttribute("initiator", initiator);
+ }
+
+ public boolean isAction(String action) {
+ return action.equalsIgnoreCase(this.getAction());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
new file mode 100644
index 00000000..610d5e76
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Reason extends Element {
+ private Reason(String name) {
+ super(name);
+ }
+
+ public Reason() {
+ super("reason");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
new file mode 100644
index 00000000..154fadf6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
@@ -0,0 +1,71 @@
+package eu.siacs.conversations.xmpp.pep;
+
+import eu.siacs.conversations.xml.Element;
+import android.util.Base64;
+
+public class Avatar {
+ public String type;
+ public String sha1sum;
+ public String image;
+ public int height;
+ public int width;
+ public long size;
+ public String owner;
+
+ public byte[] getImageAsBytes() {
+ return Base64.decode(image, Base64.DEFAULT);
+ }
+
+ public String getFilename() {
+ if (type == null) {
+ return sha1sum;
+ } else if (type.equalsIgnoreCase("image/webp")) {
+ return sha1sum + ".webp";
+ } else if (type.equalsIgnoreCase("image/png")) {
+ return sha1sum + ".png";
+ } else {
+ return sha1sum;
+ }
+ }
+
+ public static Avatar parseMetadata(Element items) {
+ Element item = items.findChild("item");
+ if (item == null) {
+ return null;
+ }
+ Element metadata = item.findChild("metadata");
+ if (metadata == null) {
+ return null;
+ }
+ String primaryId = item.getAttribute("id");
+ if (primaryId == null) {
+ return null;
+ }
+ for (Element child : metadata.getChildren()) {
+ if (child.getName().equals("info")
+ && primaryId.equals(child.getAttribute("id"))) {
+ Avatar avatar = new Avatar();
+ String height = child.getAttribute("height");
+ String width = child.getAttribute("width");
+ String size = child.getAttribute("bytes");
+ try {
+ if (height != null) {
+ avatar.height = Integer.parseInt(height);
+ }
+ if (width != null) {
+ avatar.width = Integer.parseInt(width);
+ }
+ if (size != null) {
+ avatar.size = Long.parseLong(size);
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ avatar.type = child.getAttribute("type");
+ avatar.sha1sum = child.getAttribute("id");
+ return avatar;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
new file mode 100644
index 00000000..eef41c79
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
@@ -0,0 +1,34 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class AbstractStanza extends Element {
+
+ protected AbstractStanza(String name) {
+ super(name);
+ }
+
+ public String getTo() {
+ return getAttribute("to");
+ }
+
+ public String getFrom() {
+ return getAttribute("from");
+ }
+
+ public String getId() {
+ return this.getAttribute("id");
+ }
+
+ public void setTo(String to) {
+ setAttribute("to", to);
+ }
+
+ public void setFrom(String from) {
+ setAttribute("from", from);
+ }
+
+ public void setId(String id) {
+ setAttribute("id", id);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
new file mode 100644
index 00000000..9df05e67
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
@@ -0,0 +1,76 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class IqPacket extends AbstractStanza {
+
+ public static final int TYPE_ERROR = -1;
+ public static final int TYPE_SET = 0;
+ public static final int TYPE_RESULT = 1;
+ public static final int TYPE_GET = 2;
+
+ private IqPacket(String name) {
+ super(name);
+ }
+
+ public IqPacket(int type) {
+ super("iq");
+ switch (type) {
+ case TYPE_SET:
+ this.setAttribute("type", "set");
+ break;
+ case TYPE_GET:
+ this.setAttribute("type", "get");
+ break;
+ case TYPE_RESULT:
+ this.setAttribute("type", "result");
+ break;
+ case TYPE_ERROR:
+ this.setAttribute("type", "error");
+ break;
+ default:
+ break;
+ }
+ }
+
+ public IqPacket() {
+ super("iq");
+ }
+
+ public Element query() {
+ Element query = findChild("query");
+ if (query == null) {
+ query = addChild("query");
+ }
+ return query;
+ }
+
+ public Element query(String xmlns) {
+ Element query = query();
+ query.setAttribute("xmlns", xmlns);
+ return query();
+ }
+
+ public int getType() {
+ String type = getAttribute("type");
+ if ("error".equals(type)) {
+ return TYPE_ERROR;
+ } else if ("result".equals(type)) {
+ return TYPE_RESULT;
+ } else if ("set".equals(type)) {
+ return TYPE_SET;
+ } else if ("get".equals(type)) {
+ return TYPE_GET;
+ } else {
+ return 1000;
+ }
+ }
+
+ public IqPacket generateRespone(int type) {
+ IqPacket packet = new IqPacket(type);
+ packet.setTo(this.getFrom());
+ packet.setId(this.getId());
+ return packet;
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
new file mode 100644
index 00000000..4e7b532b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
@@ -0,0 +1,66 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class MessagePacket extends AbstractStanza {
+ public static final int TYPE_CHAT = 0;
+ public static final int TYPE_NORMAL = 2;
+ public static final int TYPE_GROUPCHAT = 3;
+ public static final int TYPE_ERROR = 4;
+ public static final int TYPE_HEADLINE = 5;
+
+ public MessagePacket() {
+ super("message");
+ }
+
+ public String getBody() {
+ Element body = this.findChild("body");
+ if (body != null) {
+ return body.getContent();
+ } else {
+ return null;
+ }
+ }
+
+ public void setBody(String text) {
+ this.children.remove(findChild("body"));
+ Element body = new Element("body");
+ body.setContent(text);
+ this.children.add(body);
+ }
+
+ public void setType(int type) {
+ switch (type) {
+ case TYPE_CHAT:
+ this.setAttribute("type", "chat");
+ break;
+ case TYPE_GROUPCHAT:
+ this.setAttribute("type", "groupchat");
+ break;
+ case TYPE_NORMAL:
+ break;
+ default:
+ this.setAttribute("type", "chat");
+ break;
+ }
+ }
+
+ public int getType() {
+ String type = getAttribute("type");
+ if (type == null) {
+ return TYPE_NORMAL;
+ } else if (type.equals("normal")) {
+ return TYPE_NORMAL;
+ } else if (type.equals("chat")) {
+ return TYPE_CHAT;
+ } else if (type.equals("groupchat")) {
+ return TYPE_GROUPCHAT;
+ } else if (type.equals("error")) {
+ return TYPE_ERROR;
+ } else if (type.equals("headline")) {
+ return TYPE_HEADLINE;
+ } else {
+ return TYPE_NORMAL;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
new file mode 100644
index 00000000..7ea32099
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+public class PresencePacket extends AbstractStanza {
+
+ public PresencePacket() {
+ super("presence");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java
new file mode 100644
index 00000000..78ab66d8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java
@@ -0,0 +1,10 @@
+package eu.siacs.conversations.xmpp.stanzas.csi;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class ActivePacket extends AbstractStanza {
+ public ActivePacket() {
+ super("active");
+ setAttribute("xmlns", "urn:xmpp:csi:0");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java
new file mode 100644
index 00000000..f109280f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java
@@ -0,0 +1,10 @@
+package eu.siacs.conversations.xmpp.stanzas.csi;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class InactivePacket extends AbstractStanza {
+ public InactivePacket() {
+ super("inactive");
+ setAttribute("xmlns", "urn:xmpp:csi:0");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java
new file mode 100644
index 00000000..f93b5d87
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class AckPacket extends AbstractStanza {
+
+ public AckPacket(int sequence, int smVersion) {
+ super("a");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("h", Integer.toString(sequence));
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java
new file mode 100644
index 00000000..78cd81ed
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class EnablePacket extends AbstractStanza {
+
+ public EnablePacket(int smVersion) {
+ super("enable");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("resume", "true");
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java
new file mode 100644
index 00000000..98cfc748
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java
@@ -0,0 +1,12 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class RequestPacket extends AbstractStanza {
+
+ public RequestPacket(int smVersion) {
+ super("r");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java
new file mode 100644
index 00000000..9cdcfa5e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java
@@ -0,0 +1,14 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class ResumePacket extends AbstractStanza {
+
+ public ResumePacket(String id, int sequence, int smVersion) {
+ super("resume");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("previd", id);
+ this.setAttribute("h", Integer.toString(sequence));
+ }
+
+}
diff --git a/src/main/res/drawable-hdpi/ic_action_add_group.png b/src/main/res/drawable-hdpi/ic_action_add_group.png
new file mode 100644
index 00000000..97640355
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_add_group.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_add_person.png b/src/main/res/drawable-hdpi/ic_action_add_person.png
new file mode 100644
index 00000000..9d88d0f4
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_add_person.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_chat.png b/src/main/res/drawable-hdpi/ic_action_chat.png
new file mode 100644
index 00000000..0847ac46
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_chat.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_copy.png b/src/main/res/drawable-hdpi/ic_action_copy.png
new file mode 100644
index 00000000..22327391
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_copy.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_discard.png b/src/main/res/drawable-hdpi/ic_action_discard.png
new file mode 100644
index 00000000..703b31f8
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_discard.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_edit.png b/src/main/res/drawable-hdpi/ic_action_edit.png
new file mode 100644
index 00000000..756db316
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_edit.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_edit_dark.png b/src/main/res/drawable-hdpi/ic_action_edit_dark.png
new file mode 100644
index 00000000..5f7c6eff
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_edit_dark.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_group.png b/src/main/res/drawable-hdpi/ic_action_group.png
new file mode 100644
index 00000000..3e7f16d5
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_group.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_new.png b/src/main/res/drawable-hdpi/ic_action_new.png
new file mode 100644
index 00000000..d866d616
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_new.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_new_attachment.png b/src/main/res/drawable-hdpi/ic_action_new_attachment.png
new file mode 100644
index 00000000..c01c2b38
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_new_attachment.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_not_secure.png b/src/main/res/drawable-hdpi/ic_action_not_secure.png
new file mode 100644
index 00000000..2c917615
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_not_secure.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_refresh.png b/src/main/res/drawable-hdpi/ic_action_refresh.png
new file mode 100644
index 00000000..45b22282
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_refresh.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_remove.png b/src/main/res/drawable-hdpi/ic_action_remove.png
new file mode 100644
index 00000000..58a56e45
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_remove.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_search.png b/src/main/res/drawable-hdpi/ic_action_search.png
new file mode 100644
index 00000000..772e3598
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_search.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_secure.png b/src/main/res/drawable-hdpi/ic_action_secure.png
new file mode 100644
index 00000000..4439d1ae
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_secure.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_away.png b/src/main/res/drawable-hdpi/ic_action_send_now_away.png
new file mode 100644
index 00000000..505cbe63
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_send_now_away.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png
new file mode 100644
index 00000000..a376524d
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_offline.png b/src/main/res/drawable-hdpi/ic_action_send_now_offline.png
new file mode 100644
index 00000000..d4d2d510
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_send_now_offline.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_online.png b/src/main/res/drawable-hdpi/ic_action_send_now_online.png
new file mode 100644
index 00000000..48676f7b
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_action_send_now_online.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_activity.png b/src/main/res/drawable-hdpi/ic_activity.png
new file mode 100644
index 00000000..613da683
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_activity.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_indicator.png b/src/main/res/drawable-hdpi/ic_indicator.png
new file mode 100644
index 00000000..6de8969f
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_launcher.png b/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 00000000..d48df2c3
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_notification.png b/src/main/res/drawable-hdpi/ic_notification.png
new file mode 100644
index 00000000..664ba535
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_notification.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_profile.png b/src/main/res/drawable-hdpi/ic_profile.png
new file mode 100644
index 00000000..3f071dec
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_profile.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_received_indicator.png b/src/main/res/drawable-hdpi/ic_received_indicator.png
new file mode 100644
index 00000000..b1e3f274
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_received_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_secure_indicator.png b/src/main/res/drawable-hdpi/ic_secure_indicator.png
new file mode 100644
index 00000000..2a2934fb
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_secure_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/tab_selected_conversations.9.png b/src/main/res/drawable-hdpi/tab_selected_conversations.9.png
new file mode 100644
index 00000000..b8f44c21
--- /dev/null
+++ b/src/main/res/drawable-hdpi/tab_selected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png
new file mode 100644
index 00000000..5512dbd3
--- /dev/null
+++ b/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png
new file mode 100644
index 00000000..e5f1df22
--- /dev/null
+++ b/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png
new file mode 100644
index 00000000..7cd46d63
--- /dev/null
+++ b/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png
new file mode 100644
index 00000000..438ecdd8
--- /dev/null
+++ b/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png
new file mode 100644
index 00000000..4f18a95a
--- /dev/null
+++ b/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_add_group.png b/src/main/res/drawable-mdpi/ic_action_add_group.png
new file mode 100644
index 00000000..9a655899
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_add_group.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_add_person.png b/src/main/res/drawable-mdpi/ic_action_add_person.png
new file mode 100644
index 00000000..b7d8f46a
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_add_person.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_chat.png b/src/main/res/drawable-mdpi/ic_action_chat.png
new file mode 100644
index 00000000..8fdb5d75
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_chat.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_copy.png b/src/main/res/drawable-mdpi/ic_action_copy.png
new file mode 100644
index 00000000..71348202
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_copy.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_discard.png b/src/main/res/drawable-mdpi/ic_action_discard.png
new file mode 100644
index 00000000..248fb09c
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_discard.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_edit.png b/src/main/res/drawable-mdpi/ic_action_edit.png
new file mode 100644
index 00000000..68a45320
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_edit.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_edit_dark.png b/src/main/res/drawable-mdpi/ic_action_edit_dark.png
new file mode 100644
index 00000000..650b4d89
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_edit_dark.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_group.png b/src/main/res/drawable-mdpi/ic_action_group.png
new file mode 100644
index 00000000..1ee3cccd
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_group.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_new.png b/src/main/res/drawable-mdpi/ic_action_new.png
new file mode 100644
index 00000000..f17e7980
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_new.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_new_attachment.png b/src/main/res/drawable-mdpi/ic_action_new_attachment.png
new file mode 100644
index 00000000..1d265aac
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_new_attachment.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_not_secure.png b/src/main/res/drawable-mdpi/ic_action_not_secure.png
new file mode 100644
index 00000000..faffa233
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_not_secure.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_refresh.png b/src/main/res/drawable-mdpi/ic_action_refresh.png
new file mode 100644
index 00000000..de008e51
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_refresh.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_remove.png b/src/main/res/drawable-mdpi/ic_action_remove.png
new file mode 100644
index 00000000..342a79de
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_remove.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_search.png b/src/main/res/drawable-mdpi/ic_action_search.png
new file mode 100644
index 00000000..4edb1ff9
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_search.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_secure.png b/src/main/res/drawable-mdpi/ic_action_secure.png
new file mode 100644
index 00000000..05332ebf
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_secure.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_away.png b/src/main/res/drawable-mdpi/ic_action_send_now_away.png
new file mode 100644
index 00000000..0fdca901
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_send_now_away.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png
new file mode 100644
index 00000000..c0aef36c
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_offline.png b/src/main/res/drawable-mdpi/ic_action_send_now_offline.png
new file mode 100644
index 00000000..7723f4aa
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_send_now_offline.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_online.png b/src/main/res/drawable-mdpi/ic_action_send_now_online.png
new file mode 100644
index 00000000..39d00ee4
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_action_send_now_online.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_activity.png b/src/main/res/drawable-mdpi/ic_activity.png
new file mode 100644
index 00000000..c8727f57
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_activity.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_indicator.png b/src/main/res/drawable-mdpi/ic_indicator.png
new file mode 100644
index 00000000..bb4fee10
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_launcher.png b/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 00000000..200daf4c
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_notification.png b/src/main/res/drawable-mdpi/ic_notification.png
new file mode 100644
index 00000000..5d1aca10
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_notification.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_profile.png b/src/main/res/drawable-mdpi/ic_profile.png
new file mode 100644
index 00000000..0d056c7c
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_profile.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_received_indicator.png b/src/main/res/drawable-mdpi/ic_received_indicator.png
new file mode 100644
index 00000000..88ff1efb
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_received_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_secure_indicator.png b/src/main/res/drawable-mdpi/ic_secure_indicator.png
new file mode 100644
index 00000000..5a73aef4
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_secure_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/tab_selected_conversations.9.png b/src/main/res/drawable-mdpi/tab_selected_conversations.9.png
new file mode 100644
index 00000000..09d42dc8
--- /dev/null
+++ b/src/main/res/drawable-mdpi/tab_selected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png
new file mode 100644
index 00000000..20af01de
--- /dev/null
+++ b/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png
new file mode 100644
index 00000000..13a878be
--- /dev/null
+++ b/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png
new file mode 100644
index 00000000..ad2dbae9
--- /dev/null
+++ b/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png
new file mode 100644
index 00000000..dfff5ac8
--- /dev/null
+++ b/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png
new file mode 100644
index 00000000..4365d178
--- /dev/null
+++ b/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_add_group.png b/src/main/res/drawable-xhdpi/ic_action_add_group.png
new file mode 100644
index 00000000..c493aa5a
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_add_group.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_add_person.png b/src/main/res/drawable-xhdpi/ic_action_add_person.png
new file mode 100644
index 00000000..4e8de1b6
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_add_person.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_chat.png b/src/main/res/drawable-xhdpi/ic_action_chat.png
new file mode 100644
index 00000000..8a9a4314
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_chat.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_copy.png b/src/main/res/drawable-xhdpi/ic_action_copy.png
new file mode 100644
index 00000000..5ddf1513
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_copy.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_discard.png b/src/main/res/drawable-xhdpi/ic_action_discard.png
new file mode 100644
index 00000000..9eeeed12
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_discard.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_edit.png b/src/main/res/drawable-xhdpi/ic_action_edit.png
new file mode 100644
index 00000000..67e056fe
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_edit.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_edit_dark.png b/src/main/res/drawable-xhdpi/ic_action_edit_dark.png
new file mode 100644
index 00000000..8ab436d8
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_edit_dark.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_group.png b/src/main/res/drawable-xhdpi/ic_action_group.png
new file mode 100644
index 00000000..fa2af497
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_group.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_new.png b/src/main/res/drawable-xhdpi/ic_action_new.png
new file mode 100644
index 00000000..dde2141f
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_new.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_new_attachment.png b/src/main/res/drawable-xhdpi/ic_action_new_attachment.png
new file mode 100644
index 00000000..41cbab20
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_new_attachment.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_not_secure.png b/src/main/res/drawable-xhdpi/ic_action_not_secure.png
new file mode 100644
index 00000000..c0902a03
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_not_secure.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_refresh.png b/src/main/res/drawable-xhdpi/ic_action_refresh.png
new file mode 100644
index 00000000..cdc160d4
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_refresh.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_remove.png b/src/main/res/drawable-xhdpi/ic_action_remove.png
new file mode 100644
index 00000000..58e2e3b4
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_remove.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_search.png b/src/main/res/drawable-xhdpi/ic_action_search.png
new file mode 100644
index 00000000..19658e4a
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_search.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_secure.png b/src/main/res/drawable-xhdpi/ic_action_secure.png
new file mode 100644
index 00000000..4e08b95a
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_secure.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_away.png b/src/main/res/drawable-xhdpi/ic_action_send_now_away.png
new file mode 100644
index 00000000..bb999d85
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_send_now_away.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png
new file mode 100644
index 00000000..a0bf5561
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png b/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png
new file mode 100644
index 00000000..6da9ff7b
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_online.png b/src/main/res/drawable-xhdpi/ic_action_send_now_online.png
new file mode 100644
index 00000000..348ba657
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_action_send_now_online.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_activity.png b/src/main/res/drawable-xhdpi/ic_activity.png
new file mode 100644
index 00000000..95ffbecf
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_activity.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_indicator.png b/src/main/res/drawable-xhdpi/ic_indicator.png
new file mode 100644
index 00000000..3e5141c2
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_launcher.png b/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..927a2d2a
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_notification.png b/src/main/res/drawable-xhdpi/ic_notification.png
new file mode 100644
index 00000000..dfa643d0
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_notification.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_profile.png b/src/main/res/drawable-xhdpi/ic_profile.png
new file mode 100644
index 00000000..88a82cf0
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_profile.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_received_indicator.png b/src/main/res/drawable-xhdpi/ic_received_indicator.png
new file mode 100644
index 00000000..2c871933
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_received_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_secure_indicator.png b/src/main/res/drawable-xhdpi/ic_secure_indicator.png
new file mode 100644
index 00000000..1f4c9a32
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_secure_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png b/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png
new file mode 100644
index 00000000..34eb4ec0
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png
new file mode 100644
index 00000000..3155ef69
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png
new file mode 100644
index 00000000..5c2440e4
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png
new file mode 100644
index 00000000..e9ab742e
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png
new file mode 100644
index 00000000..42a2191e
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png
new file mode 100644
index 00000000..a5a2c25e
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_add_group.png b/src/main/res/drawable-xxhdpi/ic_action_add_group.png
new file mode 100644
index 00000000..2b46dbb9
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_add_group.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_add_person.png b/src/main/res/drawable-xxhdpi/ic_action_add_person.png
new file mode 100644
index 00000000..e9a58eaf
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_add_person.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_chat.png b/src/main/res/drawable-xxhdpi/ic_action_chat.png
new file mode 100644
index 00000000..04000fd0
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_chat.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_copy.png b/src/main/res/drawable-xxhdpi/ic_action_copy.png
new file mode 100644
index 00000000..a0508df8
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_copy.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_discard.png b/src/main/res/drawable-xxhdpi/ic_action_discard.png
new file mode 100644
index 00000000..cb1260a4
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_discard.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_edit.png b/src/main/res/drawable-xxhdpi/ic_action_edit.png
new file mode 100644
index 00000000..3a241ea4
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_edit.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png b/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png
new file mode 100644
index 00000000..f2b2078b
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_group.png b/src/main/res/drawable-xxhdpi/ic_action_group.png
new file mode 100644
index 00000000..9289b1c8
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_group.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_new.png b/src/main/res/drawable-xxhdpi/ic_action_new.png
new file mode 100644
index 00000000..c42c2bfb
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_new.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png b/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png
new file mode 100644
index 00000000..ce7536cb
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_not_secure.png b/src/main/res/drawable-xxhdpi/ic_action_not_secure.png
new file mode 100644
index 00000000..a186f1fb
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_not_secure.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_refresh.png b/src/main/res/drawable-xxhdpi/ic_action_refresh.png
new file mode 100644
index 00000000..cb847f37
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_refresh.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_remove.png b/src/main/res/drawable-xxhdpi/ic_action_remove.png
new file mode 100644
index 00000000..331c545b
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_remove.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_search.png b/src/main/res/drawable-xxhdpi/ic_action_search.png
new file mode 100644
index 00000000..a1086388
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_search.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_secure.png b/src/main/res/drawable-xxhdpi/ic_action_secure.png
new file mode 100644
index 00000000..ccf1fb00
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_secure.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png
new file mode 100644
index 00000000..12ec4d33
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png
new file mode 100644
index 00000000..7719f81a
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png
new file mode 100644
index 00000000..18895813
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png
new file mode 100644
index 00000000..29bde36e
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_activity.png b/src/main/res/drawable-xxhdpi/ic_activity.png
new file mode 100644
index 00000000..0b642d9b
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_activity.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_indicator.png b/src/main/res/drawable-xxhdpi/ic_indicator.png
new file mode 100644
index 00000000..2c51b8b7
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_launcher.png b/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..65c1af34
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_notification.png b/src/main/res/drawable-xxhdpi/ic_notification.png
new file mode 100644
index 00000000..ee1e9534
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_notification.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_profile.png b/src/main/res/drawable-xxhdpi/ic_profile.png
new file mode 100644
index 00000000..309dc513
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_profile.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_received_indicator.png b/src/main/res/drawable-xxhdpi/ic_received_indicator.png
new file mode 100644
index 00000000..039a9ef9
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_received_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_secure_indicator.png b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png
new file mode 100644
index 00000000..1ee9b67d
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png
new file mode 100644
index 00000000..e4439e7c
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png
new file mode 100644
index 00000000..dd2ded89
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png
new file mode 100644
index 00000000..58c8a576
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png
new file mode 100644
index 00000000..566062f0
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png
new file mode 100644
index 00000000..432e68c4
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png
new file mode 100644
index 00000000..8dd01d5c
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png
Binary files differ
diff --git a/src/main/res/drawable/actionbar_tab_indicator.xml b/src/main/res/drawable/actionbar_tab_indicator.xml
new file mode 100644
index 00000000..5598ee42
--- /dev/null
+++ b/src/main/res/drawable/actionbar_tab_indicator.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Non focused states -->
+ <item android:drawable="@android:color/transparent" android:state_focused="false" android:state_pressed="false" android:state_selected="false"/>
+ <item android:drawable="@drawable/tab_selected_conversations" android:state_focused="false" android:state_pressed="false" android:state_selected="true"/>
+
+ <!-- Focused states -->
+ <item android:drawable="@drawable/tab_unselected_focused_conversations" android:state_focused="true" android:state_pressed="false" android:state_selected="false"/>
+ <item android:drawable="@drawable/tab_selected_focused_conversations" android:state_focused="true" android:state_pressed="false" android:state_selected="true"/>
+
+ <!-- Pressed -->
+ <!-- Non focused states -->
+ <item android:drawable="@drawable/tab_unselected_pressed_conversations" android:state_focused="false" android:state_pressed="true" android:state_selected="false"/>
+ <item android:drawable="@drawable/tab_selected_pressed_conversations" android:state_focused="false" android:state_pressed="true" android:state_selected="true"/>
+
+ <!-- Focused states -->
+ <item android:drawable="@drawable/tab_unselected_pressed_conversations" android:state_focused="true" android:state_pressed="true" android:state_selected="false"/>
+ <item android:drawable="@drawable/tab_selected_pressed_conversations" android:state_focused="true" android:state_pressed="true" android:state_selected="true"/>
+
+</selector> \ No newline at end of file
diff --git a/src/main/res/drawable/es_slidingpane_shadow.xml b/src/main/res/drawable/es_slidingpane_shadow.xml
new file mode 100644
index 00000000..44ffd4ea
--- /dev/null
+++ b/src/main/res/drawable/es_slidingpane_shadow.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <gradient
+ android:endColor="@color/divider"
+ android:startColor="@android:color/transparent" />
+
+ <size
+ android:height="0.5dp"
+ android:width="3.0dp" />
+
+</shape> \ No newline at end of file
diff --git a/src/main/res/drawable/grey.xml b/src/main/res/drawable/grey.xml
new file mode 100644
index 00000000..2e90d96d
--- /dev/null
+++ b/src/main/res/drawable/grey.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+
+ <solid android:color="#ffdddddd" />
+
+</shape> \ No newline at end of file
diff --git a/src/main/res/drawable/greybackground.xml b/src/main/res/drawable/greybackground.xml
new file mode 100644
index 00000000..bedc4b17
--- /dev/null
+++ b/src/main/res/drawable/greybackground.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:drawable="@drawable/grey" android:state_pressed="true"/>
+
+</selector> \ No newline at end of file
diff --git a/src/main/res/drawable/infocard_border.xml b/src/main/res/drawable/infocard_border.xml
new file mode 100644
index 00000000..af7d5d22
--- /dev/null
+++ b/src/main/res/drawable/infocard_border.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <solid android:color="@color/primarybackground" />
+
+ <corners android:radius="2dp" />
+
+ <stroke
+ android:width="0.5dp"
+ android:color="@color/divider" >
+ </stroke>
+
+ <padding
+ android:bottom="0dp"
+ android:left="0dp"
+ android:right="0dp"
+ android:top="0dp" />
+
+</shape> \ No newline at end of file
diff --git a/src/main/res/drawable/message_border.xml b/src/main/res/drawable/message_border.xml
new file mode 100644
index 00000000..b35693d5
--- /dev/null
+++ b/src/main/res/drawable/message_border.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+
+ <corners android:radius="2dp" />
+
+ <padding
+ android:bottom="1.5dp"
+ android:left="1.5dp"
+ android:right="1.5dp"
+ android:top="1.5dp" />
+
+ <solid android:color="@color/divider" />
+
+</shape> \ No newline at end of file
diff --git a/src/main/res/drawable/snackbar.xml b/src/main/res/drawable/snackbar.xml
new file mode 100644
index 00000000..13818618
--- /dev/null
+++ b/src/main/res/drawable/snackbar.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <solid android:color="@color/darkbackground" />
+
+ <corners android:radius="8dip" />
+
+ <padding
+ android:bottom="0dip"
+ android:left="0dip"
+ android:right="0dip"
+ android:top="0dip" />
+
+</shape> \ No newline at end of file
diff --git a/src/main/res/layout-w360dp/fragment_conversations_overview.xml b/src/main/res/layout-w360dp/fragment_conversations_overview.xml
new file mode 100644
index 00000000..a600118d
--- /dev/null
+++ b/src/main/res/layout-w360dp/fragment_conversations_overview.xml
@@ -0,0 +1,30 @@
+<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_view_spl"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="300dp"
+ android:layout_height="match_parent"
+ android:background="@color/primarybackground"
+ android:orientation="vertical" >
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/primarybackground"
+ android:divider="@color/divider"
+ android:dividerHeight="1dp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/selected_conversation"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical" >
+ </LinearLayout>
+
+</android.support.v4.widget.SlidingPaneLayout> \ No newline at end of file
diff --git a/src/main/res/layout-w384dp/fragment_conversations_overview.xml b/src/main/res/layout-w384dp/fragment_conversations_overview.xml
new file mode 100644
index 00000000..c3aa67ae
--- /dev/null
+++ b/src/main/res/layout-w384dp/fragment_conversations_overview.xml
@@ -0,0 +1,30 @@
+<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_view_spl"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="345dp"
+ android:layout_height="match_parent"
+ android:background="@color/primarybackground"
+ android:orientation="vertical" >
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/primarybackground"
+ android:divider="@color/divider"
+ android:dividerHeight="1dp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/selected_conversation"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical" >
+ </LinearLayout>
+
+</android.support.v4.widget.SlidingPaneLayout> \ No newline at end of file
diff --git a/src/main/res/layout-w600dp/fragment_conversations_overview.xml b/src/main/res/layout-w600dp/fragment_conversations_overview.xml
new file mode 100644
index 00000000..331fb1f0
--- /dev/null
+++ b/src/main/res/layout-w600dp/fragment_conversations_overview.xml
@@ -0,0 +1,30 @@
+<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_view_spl"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="400dp"
+ android:layout_height="match_parent"
+ android:background="@color/primarybackground"
+ android:orientation="vertical" >
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/primarybackground"
+ android:divider="@color/divider"
+ android:dividerHeight="1dp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/selected_conversation"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical" >
+ </LinearLayout>
+
+</android.support.v4.widget.SlidingPaneLayout> \ No newline at end of file
diff --git a/src/main/res/layout-w960dp/fragment_conversations_overview.xml b/src/main/res/layout-w960dp/fragment_conversations_overview.xml
new file mode 100644
index 00000000..2744f38e
--- /dev/null
+++ b/src/main/res/layout-w960dp/fragment_conversations_overview.xml
@@ -0,0 +1,32 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_view_ll"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:baselineAligned="false">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="@color/primarybackground"
+ android:orientation="vertical" >
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/primarybackground"
+ android:divider="@color/divider"
+ android:dividerHeight="1dp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/selected_conversation"
+ android:layout_width="0dp"
+ android:layout_weight="2"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml
new file mode 100644
index 00000000..2d1190a3
--- /dev/null
+++ b/src/main/res/layout/account_row.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/activatedBackgroundIndicator"
+ android:padding="8dp" >
+
+ <ImageView
+ android:id="@+id/account_image"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentLeft="true"
+ android:src="@drawable/ic_profile" >
+ </ImageView>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@+id/account_image"
+ android:orientation="vertical"
+ android:paddingLeft="8dp" >
+
+ <TextView
+ android:id="@+id/account_jid"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scrollHorizontally="false"
+ android:singleLine="true"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline" />
+
+ <TextView
+ android:id="@+id/account_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/account_status_unknown"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody"
+ android:textStyle="bold" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/actionview_search.xml b/src/main/res/layout/actionview_search.xml
new file mode 100644
index 00000000..64b75f0e
--- /dev/null
+++ b/src/main/res/layout/actionview_search.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:addStatesFromChildren="true"
+ android:focusable="true"
+ android:gravity="center"
+ android:paddingLeft="5dp"
+ android:paddingRight="5dp" >
+
+ <EditText
+ android:id="@+id/search_field"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:inputType="textEmailAddress|textNoSuggestions"
+ android:textColor="@color/ondarktext" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/activity_choose_contact.xml b/src/main/res/layout/activity_choose_contact.xml
new file mode 100644
index 00000000..248a7822
--- /dev/null
+++ b/src/main/res/layout/activity_choose_contact.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <ListView
+ android:id="@+id/choose_contact_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:listitem="@layout/contact" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml
new file mode 100644
index 00000000..f7cb2198
--- /dev/null
+++ b/src/main/res/layout/activity_contact_details.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@color/secondarybackground" >
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:background="@drawable/infocard_border"
+ android:padding="16dp" >
+
+ <QuickContactBadge
+ android:id="@+id/details_contact_badge"
+ android:layout_width="72dp"
+ android:layout_height="72dp"
+ android:layout_alignParentTop="true"
+ android:scaleType="centerCrop" />
+
+ <LinearLayout
+ android:id="@+id/details_jidbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_toRightOf="@+id/details_contact_badge"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/details_contactjid"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/account_settings_example_jabber_id"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline"
+ android:textStyle="bold" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/details_contactstatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=" · "
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/details_lastseen"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </LinearLayout>
+
+ <CheckBox
+ android:id="@+id/details_send_presence"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/send_presence_updates"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <CheckBox
+ android:id="@+id/details_receive_presence"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/receive_presence_updates"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/details_account"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_below="@+id/details_jidbox"
+ android:layout_marginTop="32dp"
+ android:text="@string/using_account"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeInfo" />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/details_contact_keys"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:background="@drawable/infocard_border"
+ android:divider="?android:dividerHorizontal"
+ android:orientation="vertical"
+ android:padding="8dp"
+ android:showDividers="middle" >
+ </LinearLayout>
+ </LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml
new file mode 100644
index 00000000..97289628
--- /dev/null
+++ b/src/main/res/layout/activity_edit_account.xml
@@ -0,0 +1,272 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/secondarybackground" >
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/button_bar"
+ android:layout_alignParentTop="true" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:id="@+id/editor"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:background="@drawable/infocard_border"
+ android:orientation="vertical"
+ android:padding="16dp" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/account_settings_jabber_id"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <AutoCompleteTextView
+ android:id="@+id/account_jid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/account_settings_example_jabber_id"
+ android:inputType="textEmailAddress"
+ android:textColor="@color/primarytext"
+ android:textColorHint="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/account_settings_password"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <EditText
+ android:id="@+id/account_password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password"
+ android:inputType="textPassword"
+ android:textColor="@color/primarytext"
+ android:textColorHint="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <CheckBox
+ android:id="@+id/account_register_new"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/register_account"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/account_confirm_password_desc"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/account_settings_confirm_password"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody"
+ android:visibility="gone" />
+
+ <EditText
+ android:id="@+id/account_password_confirm"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:hint="@string/confirm_password"
+ android:inputType="textPassword"
+ android:visibility="gone"
+ android:textColor="@color/primarytext"
+ android:textColorHint="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/stats"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_margin="8dp"
+ android:background="@drawable/infocard_border"
+ android:orientation="vertical"
+ android:padding="16dp"
+ android:visibility="gone" >
+
+ <TableLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:stretchColumns="1" >
+
+ <TableRow
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/server_info_session_established"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/session_est"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </TableRow>
+
+ <TableRow
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/server_info_pep"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/server_info_pep"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </TableRow>
+
+ <TableRow
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/server_info_stream_management"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/server_info_sm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </TableRow>
+
+ <TableRow
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/server_info_carbon_messages"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/server_info_carbons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </TableRow>
+ </TableLayout>
+
+
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/otr_fingerprint_box"
+ android:layout_marginTop="32dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@+id/action_copy_to_clipboard"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/otr_fingerprint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody"
+ android:typeface="monospace" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeInfo"
+ android:text="@string/otr_fingerprint"/>
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/action_copy_to_clipboard"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:padding="8dp"
+ android:src="@drawable/ic_action_copy"
+ android:visibility="visible" />
+ </RelativeLayout>
+
+
+ </LinearLayout>
+ </LinearLayout>
+ </ScrollView>
+
+ <LinearLayout
+ android:id="@+id/button_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true" >
+
+ <Button
+ android:id="@+id/cancel_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/cancel"
+ android:textColor="@color/primarytext" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="fill_parent"
+ android:layout_marginBottom="7dp"
+ android:layout_marginTop="7dp"
+ android:background="@color/divider" />
+
+ <Button
+ android:id="@+id/save_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:enabled="false"
+ android:text="@string/save"
+ android:textColor="@color/secondarytext" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml
new file mode 100644
index 00000000..f689f10d
--- /dev/null
+++ b/src/main/res/layout/activity_muc_details.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@color/secondarybackground" >
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:background="@drawable/infocard_border"
+ android:orientation="vertical"
+ android:padding="16dp" >
+
+ <TextView
+ android:id="@+id/muc_jabberid"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/account_settings_example_jabber_id"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline"
+ android:textStyle="bold"
+ android:layout_marginBottom="16dp"/>
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/your_photo"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentLeft="true"
+ android:src="@drawable/ic_profile" >
+ </ImageView>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@+id/your_photo"
+ android:orientation="vertical"
+ android:paddingLeft="8dp" >
+
+ <TextView
+ android:id="@+id/muc_your_nick"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline" />
+
+ <TextView
+ android:id="@+id/muc_role"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/edit_nick_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:padding="8dp"
+ android:src="@drawable/ic_action_edit_dark" />
+ </RelativeLayout>
+ <TextView
+ android:id="@+id/details_account"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:layout_marginTop="32dp"
+ android:text="@string/using_account"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeInfo" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/muc_more_details"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:background="@drawable/infocard_border"
+ android:orientation="vertical"
+ android:padding="8dp" >
+
+
+ <LinearLayout
+ android:id="@+id/muc_members"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:divider="?android:dividerHorizontal"
+ android:orientation="vertical"
+ android:showDividers="middle" >
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/invite"
+ style="?android:attr/buttonStyleSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="24dp"
+ android:text="@string/invite_contact" />
+ </LinearLayout>
+
+</LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml
new file mode 100644
index 00000000..fac499bc
--- /dev/null
+++ b/src/main/res/layout/activity_publish_profile_picture.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/primarybackground" >
+
+ <LinearLayout
+ android:id="@+id/account_image_wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="24dp"
+ android:background="@drawable/message_border" >
+
+ <ImageView
+ android:id="@+id/account_image"
+ android:layout_width="194dp"
+ android:layout_height="194dp" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/account_image_wrapper"
+ android:layout_centerHorizontal="true"
+ android:text="@string/touch_to_choose_picture"
+ android:textColor="@color/secondarytext" />
+
+ <TextView
+ android:id="@+id/secondary_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/hint"
+ android:layout_centerHorizontal="true"
+ android:text="@string/or_long_press_for_default"
+ android:textColor="@color/secondarytext" />
+
+ <LinearLayout
+ android:id="@+id/button_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true" >
+
+ <Button
+ android:id="@+id/cancel_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/cancel"
+ android:textColor="@color/primarytext" />
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="fill_parent"
+ android:layout_marginBottom="7dp"
+ android:layout_marginTop="7dp"
+ android:background="@color/divider" />
+
+ <Button
+ android:id="@+id/publish_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:enabled="false"
+ android:text="@string/publish"
+ android:textColor="@color/secondarytext" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_above="@+id/button_bar"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_below="@+id/secondary_hint"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp" >
+
+ <TextView
+ android:id="@+id/account"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline" />
+
+ <TextView
+ android:id="@+id/hint_or_warning"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:minLines="3"
+ android:text="@string/publish_avatar_explanation"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/activity_start_conversation.xml b/src/main/res/layout/activity_start_conversation.xml
new file mode 100644
index 00000000..f9c98529
--- /dev/null
+++ b/src/main/res/layout/activity_start_conversation.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/start_conversation_view_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/primarybackground" >
+
+</android.support.v4.view.ViewPager> \ No newline at end of file
diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml
new file mode 100644
index 00000000..12ab3da1
--- /dev/null
+++ b/src/main/res/layout/contact.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/activatedBackgroundIndicator"
+ android:padding="8dp" >
+
+ <ImageView
+ android:id="@+id/contact_photo"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentLeft="true"
+ android:scaleType="centerCrop"
+ android:src="@drawable/ic_profile" >
+ </ImageView>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@+id/contact_photo"
+ android:orientation="vertical"
+ android:paddingLeft="8dp" >
+
+ <TextView
+ android:id="@+id/contact_display_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline" />
+
+ <TextView
+ android:id="@+id/contact_jid"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/key"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline"
+ android:typeface="monospace"
+ android:visibility="gone" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml
new file mode 100644
index 00000000..7053857f
--- /dev/null
+++ b/src/main/res/layout/contact_key.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@+id/button_remove"
+ android:orientation="vertical"
+ android:padding="8dp" >
+
+ <TextView
+ android:id="@+id/key"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody"
+ android:typeface="monospace" />
+
+ <TextView
+ android:id="@+id/key_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeInfo"/>
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/button_remove"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:padding="8dp"
+ android:src="@drawable/ic_action_remove"
+ android:visibility="invisible" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/conversation_list_row.xml b/src/main/res/layout/conversation_list_row.xml
new file mode 100644
index 00000000..21147b4a
--- /dev/null
+++ b/src/main/res/layout/conversation_list_row.xml
@@ -0,0 +1,68 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="8dp" >
+
+ <ImageView
+ android:id="@+id/conversation_image"
+ android:layout_width="56dp"
+ android:layout_height="56dp"
+ android:layout_alignParentLeft="true"
+ android:scaleType="centerCrop" />
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@+id/conversation_image"
+ android:paddingLeft="8dp" >
+
+ <TextView
+ android:id="@+id/conversation_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignLeft="@+id/conversation_lastwrapper"
+ android:layout_toLeftOf="@+id/conversation_lastupdate"
+ android:singleLine="true"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline"
+ android:typeface="sans" />
+
+ <LinearLayout
+ android:id="@+id/conversation_lastwrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/conversation_name"
+ android:orientation="vertical"
+ android:paddingTop="3dp" >
+
+ <TextView
+ android:id="@+id/conversation_lastmsg"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:scrollHorizontally="false"
+ android:singleLine="true"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <ImageView
+ android:id="@+id/conversation_lastimage"
+ android:layout_width="fill_parent"
+ android:layout_height="36dp"
+ android:background="@color/primarytext"
+ android:scaleType="centerCrop" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/conversation_lastupdate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@+id/conversation_name"
+ android:layout_alignParentRight="true"
+ android:gravity="right"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeInfo" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/create_contact_dialog.xml b/src/main/res/layout/create_contact_dialog.xml
new file mode 100644
index 00000000..1ab4b686
--- /dev/null
+++ b/src/main/res/layout/create_contact_dialog.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="8dp" >
+
+ <TextView
+ android:id="@+id/your_account"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/your_account"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <Spinner
+ android:id="@+id/account"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@+id/jabber_id"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/account_settings_jabber_id"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <AutoCompleteTextView
+ android:id="@+id/jid"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/account_settings_example_jabber_id"
+ android:inputType="textEmailAddress"
+ android:textColor="@color/primarytext"
+ android:textColorHint="@color/secondarytext" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/dialog_clear_history.xml b/src/main/res/layout/dialog_clear_history.xml
new file mode 100644
index 00000000..252808c8
--- /dev/null
+++ b/src/main/res/layout/dialog_clear_history.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="8dp" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingBottom="8dp"
+ android:text="@string/clear_histor_msg"
+ android:textSize="?attr/TextSizeBody" />
+
+ <CheckBox
+ android:id="@+id/end_conversation_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/also_end_conversation" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/dialog_verify_otr.xml b/src/main/res/layout/dialog_verify_otr.xml
new file mode 100644
index 00000000..499ef6cd
--- /dev/null
+++ b/src/main/res/layout/dialog_verify_otr.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingBottom="16dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:text="@string/account_settings_jabber_id"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline" />
+
+ <TextView
+ android:id="@+id/verify_otr_jid"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dp"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:text="@string/otr_fingerprint"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline" />
+
+ <TextView
+ android:id="@+id/verify_otr_fingerprint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dp"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody"
+ android:typeface="monospace" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:text="@string/your_fingerprint"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeHeadline" />
+
+ <TextView
+ android:id="@+id/verify_otr_yourprint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dp"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeBody"
+ android:typeface="monospace" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml
new file mode 100644
index 00000000..f9aae10a
--- /dev/null
+++ b/src/main/res/layout/fragment_conversation.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/secondarybackground" >
+
+ <ListView
+ android:id="@+id/messages_view"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/snackbar"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="@color/secondarybackground"
+ android:divider="@null"
+ android:dividerHeight="0dp"
+ android:listSelector="@android:color/transparent"
+ android:stackFromBottom="true"
+ android:transcriptMode="normal"
+ tools:listitem="@layout/message_sent" >
+ </ListView>
+
+ <RelativeLayout
+ android:id="@+id/textsend"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:background="@color/primarybackground" >
+
+ <eu.siacs.conversations.ui.EditMessage
+ android:id="@+id/textinput"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@+id/textSendButton"
+ android:background="@color/primarybackground"
+ android:ems="10"
+ android:imeOptions="flagNoExtractUi|actionSend"
+ android:inputType="textShortMessage|textMultiLine|textCapSentences"
+ android:minHeight="48dp"
+ android:minLines="1"
+ android:paddingBottom="12dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingTop="12dp"
+ android:textColor="@color/primarytext" >
+
+ <requestFocus />
+ </eu.siacs.conversations.ui.EditMessage>
+
+ <ImageButton
+ android:id="@+id/textSendButton"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:src="@drawable/ic_action_send_now_offline" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/snackbar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/textsend"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:background="@drawable/snackbar"
+ android:minHeight="48dp"
+ android:visibility="gone" >
+
+ <TextView
+ android:id="@+id/snackbar_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/snackbar_action"
+ android:paddingLeft="24dp"
+ android:textColor="@color/ondarktext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <TextView
+ android:id="@+id/snackbar_action"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:paddingBottom="16dp"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="16dp"
+ android:textAllCaps="true"
+ android:textColor="@color/ondarktext"
+ android:textSize="?attr/TextSizeBody"
+ android:textStyle="bold" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/fragment_conversations_overview.xml b/src/main/res/layout/fragment_conversations_overview.xml
new file mode 100644
index 00000000..d4145761
--- /dev/null
+++ b/src/main/res/layout/fragment_conversations_overview.xml
@@ -0,0 +1,30 @@
+<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_view_spl"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="288dp"
+ android:layout_height="match_parent"
+ android:background="@color/primarybackground"
+ android:orientation="vertical" >
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/primarybackground"
+ android:divider="@color/divider"
+ android:dividerHeight="1dp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/selected_conversation"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical" >
+ </LinearLayout>
+
+</android.support.v4.widget.SlidingPaneLayout> \ No newline at end of file
diff --git a/src/main/res/layout/join_conference_dialog.xml b/src/main/res/layout/join_conference_dialog.xml
new file mode 100644
index 00000000..95c9d24c
--- /dev/null
+++ b/src/main/res/layout/join_conference_dialog.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="8dp" >
+
+ <TextView
+ android:id="@+id/your_account"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/your_account"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <Spinner
+ android:id="@+id/account"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@+id/jabber_id"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/conference_address"
+ android:textColor="@color/primarytext"
+ android:textSize="?attr/TextSizeBody" />
+
+ <AutoCompleteTextView
+ android:id="@+id/jid"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/conference_address_example"
+ android:inputType="textEmailAddress"
+ android:textColor="@color/primarytext"
+ android:textColorHint="@color/secondarytext" />
+
+ <CheckBox
+ android:id="@+id/bookmark"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:checked="true"
+ android:text="@string/save_as_bookmark" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/manage_accounts.xml b/src/main/res/layout/manage_accounts.xml
new file mode 100644
index 00000000..11ce35b2
--- /dev/null
+++ b/src/main/res/layout/manage_accounts.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@color/primarybackground" >
+
+ <ListView
+ android:id="@+id/account_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:divider="@color/divider"
+ android:dividerHeight="1dp" >
+ </ListView>
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/message_null.xml b/src/main/res/layout/message_null.xml
new file mode 100644
index 00000000..0e0f1c92
--- /dev/null
+++ b/src/main/res/layout/message_null.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:background="#00000000">
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/message_received.xml b/src/main/res/layout/message_received.xml
new file mode 100644
index 00000000..730d00d5
--- /dev/null
+++ b/src/main/res/layout/message_received.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="4dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingTop="4dp" >
+
+ <LinearLayout
+ android:id="@+id/message_box"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_toRightOf="@+id/message_photo"
+ android:background="@drawable/message_border"
+ android:minHeight="48dp" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:background="@color/primarybackground"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingBottom="4dp"
+ android:paddingLeft="5dp"
+ android:paddingRight="5dp"
+ android:paddingTop="4dp" >
+
+ <ImageView
+ android:id="@+id/message_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:background="@color/primarytext"
+ android:paddingBottom="2dp"
+ android:scaleType="centerCrop" />
+
+ <TextView
+ android:id="@+id/message_body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:autoLink="web"
+ android:textColor="@color/primarytext"
+ android:textIsSelectable="true"
+ android:textSize="?attr/TextSizeBody" />
+
+ <Button
+ android:id="@+id/download_button"
+ style="?android:attr/buttonStyleSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/download_image"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingTop="1dp" >
+
+ <ImageView
+ android:id="@+id/security_indicator"
+ android:layout_width="?attr/TextSizeInfo"
+ android:layout_height="?attr/TextSizeInfo"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="4sp"
+ android:alpha="0.54"
+ android:gravity="center_vertical"
+ android:src="@drawable/ic_secure_indicator" />
+
+ <TextView
+ android:id="@+id/message_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:gravity="center_vertical"
+ android:text="@string/sending"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeInfo" />
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/message_photo"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginRight="-1.5dp"
+ android:padding="0dp"
+ android:scaleType="fitXY"
+ android:src="@drawable/ic_profile" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml
new file mode 100644
index 00000000..e3e9b673
--- /dev/null
+++ b/src/main/res/layout/message_sent.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="4dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:paddingTop="4dp" >
+
+ <LinearLayout
+ android:id="@+id/message_box"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_toLeftOf="@+id/message_photo"
+ android:background="@drawable/message_border"
+ android:minHeight="48dp" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:background="@color/primarybackground"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingBottom="4dp"
+ android:paddingLeft="5dp"
+ android:paddingRight="5dp"
+ android:paddingTop="4dp" >
+
+ <ImageView
+ android:id="@+id/message_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:background="@color/primarytext"
+ android:paddingBottom="2dp"
+ android:scaleType="centerCrop" />
+
+ <TextView
+ android:id="@+id/message_body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:autoLink="web"
+ android:textColor="@color/primarytext"
+ android:textIsSelectable="true"
+ android:textSize="?attr/TextSizeBody" />
+
+ <Button
+ android:id="@+id/download_button"
+ style="?android:attr/buttonStyleSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/download_image"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:orientation="horizontal"
+ android:paddingTop="1dp" >
+
+ <TextView
+ android:id="@+id/message_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:gravity="center_vertical"
+ android:text="@string/sending"
+ android:textColor="@color/secondarytext"
+ android:textSize="?attr/TextSizeInfo" />
+
+ <ImageView
+ android:id="@+id/security_indicator"
+ android:layout_width="?attr/TextSizeInfo"
+ android:layout_height="?attr/TextSizeInfo"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="4sp"
+ android:alpha="0.54"
+ android:gravity="center_vertical"
+ android:src="@drawable/ic_secure_indicator" />
+
+ <ImageView
+ android:id="@+id/indicator_received"
+ android:layout_width="?attr/TextSizeInfo"
+ android:layout_height="?attr/TextSizeInfo"
+ android:layout_gravity="center_vertical"
+ android:layout_marginLeft="4sp"
+ android:alpha="0.54"
+ android:gravity="center_vertical"
+ android:src="@drawable/ic_received_indicator" />
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/message_photo"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="-1.5dp"
+ android:padding="0dp"
+ android:scaleType="fitXY"
+ android:src="@drawable/ic_profile" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/message_status.xml b/src/main/res/layout/message_status.xml
new file mode 100644
index 00000000..d5f8bb33
--- /dev/null
+++ b/src/main/res/layout/message_status.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="6dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="6dp"
+ android:paddingTop="6dp" >
+
+ <ImageView
+ android:id="@+id/message_photo"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginRight="-1.5dp"
+ android:padding="0dp"
+ android:scaleType="fitXY"
+ android:src="@drawable/ic_profile" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/src/main/res/layout/quickedit.xml b/src/main/res/layout/quickedit.xml
new file mode 100644
index 00000000..20a2868a
--- /dev/null
+++ b/src/main/res/layout/quickedit.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="16dp" >
+
+ <EditText
+ android:id="@+id/editor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textPersonName"
+ android:textColor="@color/primarytext" >
+
+ <requestFocus />
+ </EditText>
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/layout/share_with.xml b/src/main/res/layout/share_with.xml
new file mode 100644
index 00000000..41b6033d
--- /dev/null
+++ b/src/main/res/layout/share_with.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <ListView
+ android:id="@+id/choose_conversation_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:listitem="@layout/conversation_list_row" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/menu/attachment_choices.xml b/src/main/res/menu/attachment_choices.xml
new file mode 100644
index 00000000..20932489
--- /dev/null
+++ b/src/main/res/menu/attachment_choices.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/attach_choose_picture"
+ android:title="@string/attach_choose_picture"/>
+ <item
+ android:id="@+id/attach_take_picture"
+ android:title="@string/attach_take_picture"/>
+ <item
+ android:id="@+id/attach_record_voice"
+ android:title="@string/attach_record_voice"
+ android:visible="false"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/choose_contact.xml b/src/main/res/menu/choose_contact.xml
new file mode 100644
index 00000000..e39091b3
--- /dev/null
+++ b/src/main/res/menu/choose_contact.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_search"
+ android:actionLayout="@layout/actionview_search"
+ android:icon="@drawable/ic_action_search"
+ android:showAsAction="collapseActionView|always"
+ android:title="@string/search"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/conference_context.xml b/src/main/res/menu/conference_context.xml
new file mode 100644
index 00000000..fd898580
--- /dev/null
+++ b/src/main/res/menu/conference_context.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/context_join_conference"
+ android:title="@string/join_conference"/>
+ <item
+ android:id="@+id/context_delete_conference"
+ android:title="@string/delete_bookmark"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/contact_context.xml b/src/main/res/menu/contact_context.xml
new file mode 100644
index 00000000..11ac7d7c
--- /dev/null
+++ b/src/main/res/menu/contact_context.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/context_start_conversation"
+ android:title="@string/start_conversation"/>
+ <item
+ android:id="@+id/context_contact_details"
+ android:title="@string/view_contact_details"/>
+ <item
+ android:id="@+id/context_delete_contact"
+ android:title="@string/delete_contact"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/contact_details.xml b/src/main/res/menu/contact_details.xml
new file mode 100644
index 00000000..02f2e813
--- /dev/null
+++ b/src/main/res/menu/contact_details.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_edit_contact"
+ android:icon="@drawable/ic_action_edit"
+ android:orderInCategory="10"
+ android:showAsAction="always"
+ android:title="@string/action_edit_contact"/>
+ <item
+ android:id="@+id/action_delete_contact"
+ android:icon="@drawable/ic_action_discard"
+ android:orderInCategory="10"
+ android:showAsAction="always"
+ android:title="@string/action_delete_contact"/>
+ <item
+ android:id="@+id/action_accounts"
+ android:orderInCategory="90"
+ android:showAsAction="never"
+ android:title="@string/action_accounts"/>
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/action_settings"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/conversations.xml b/src/main/res/menu/conversations.xml
new file mode 100644
index 00000000..3edee120
--- /dev/null
+++ b/src/main/res/menu/conversations.xml
@@ -0,0 +1,63 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_add"
+ android:icon="@drawable/ic_action_new"
+ android:orderInCategory="10"
+ android:showAsAction="always"
+ android:title="@string/action_add"/>
+ <item
+ android:id="@+id/action_security"
+ android:icon="@drawable/ic_action_not_secure"
+ android:orderInCategory="20"
+ android:showAsAction="always"
+ android:title="@string/action_secure"/>
+ <item
+ android:id="@+id/action_attach_file"
+ android:icon="@drawable/ic_action_new_attachment"
+ android:orderInCategory="30"
+ android:showAsAction="always"
+ android:title="@string/attach_file"/>
+ <item
+ android:id="@+id/action_contact_details"
+ android:orderInCategory="40"
+ android:showAsAction="never"
+ android:title="@string/action_contact_details"/>
+ <item
+ android:id="@+id/action_muc_details"
+ android:icon="@drawable/ic_action_group"
+ android:orderInCategory="40"
+ android:showAsAction="ifRoom"
+ android:title="@string/action_muc_details"/>
+ <item
+ android:id="@+id/action_invite"
+ android:orderInCategory="45"
+ android:showAsAction="never"
+ android:title="@string/invite_contact"/>
+ <item
+ android:id="@+id/action_clear_history"
+ android:orderInCategory="50"
+ android:showAsAction="never"
+ android:title="@string/action_clear_history"/>
+ <item
+ android:id="@+id/action_archive"
+ android:orderInCategory="60"
+ android:showAsAction="never"
+ android:title="@string/action_end_conversation"/>
+ <item
+ android:id="@+id/action_mute"
+ android:orderInCategory="70"
+ android:showAsAction="never"
+ android:title="@string/disable_notifications"/>
+ <item
+ android:id="@+id/action_accounts"
+ android:orderInCategory="90"
+ android:showAsAction="never"
+ android:title="@string/action_accounts"/>
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/action_settings"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/encryption_choices.xml b/src/main/res/menu/encryption_choices.xml
new file mode 100644
index 00000000..adf0ad8d
--- /dev/null
+++ b/src/main/res/menu/encryption_choices.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <group android:checkableBehavior="single" >
+ <item
+ android:id="@+id/encryption_choice_none"
+ android:title="@string/encryption_choice_none"/>
+ <item
+ android:id="@+id/encryption_choice_otr"
+ android:title="@string/encryption_choice_otr"/>
+ <item
+ android:id="@+id/encryption_choice_pgp"
+ android:title="@string/encryption_choice_pgp"/>
+ </group>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/manageaccounts.xml b/src/main/res/menu/manageaccounts.xml
new file mode 100644
index 00000000..b5cd9b50
--- /dev/null
+++ b/src/main/res/menu/manageaccounts.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_add_account"
+ android:icon="@drawable/ic_action_add_person"
+ android:showAsAction="always"
+ android:title="@string/action_add_account"/>
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/action_settings"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/manageaccounts_context.xml b/src/main/res/menu/manageaccounts_context.xml
new file mode 100644
index 00000000..7a7cc0a2
--- /dev/null
+++ b/src/main/res/menu/manageaccounts_context.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/mgmt_account_enable"
+ android:title="@string/mgmt_account_enable"/>
+ <item
+ android:id="@+id/mgmt_account_publish_avatar"
+ android:title="@string/mgmt_account_publish_avatar"/>
+ <item
+ android:id="@+id/mgmt_account_announce_pgp"
+ android:title="@string/mgmt_account_publish_pgp"/>
+ <item
+ android:id="@+id/mgmt_account_disable"
+ android:showAsAction="never"
+ android:title="@string/mgmt_account_disable"/>
+ <item
+ android:id="@+id/mgmt_account_delete"
+ android:title="@string/mgmt_account_delete"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/muc_details.xml b/src/main/res/menu/muc_details.xml
new file mode 100644
index 00000000..97369098
--- /dev/null
+++ b/src/main/res/menu/muc_details.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_edit_subject"
+ android:icon="@drawable/ic_action_edit"
+ android:orderInCategory="10"
+ android:showAsAction="always"
+ android:title="@string/action_edit_subject"/>
+ <item
+ android:id="@+id/action_accounts"
+ android:orderInCategory="90"
+ android:showAsAction="never"
+ android:title="@string/action_accounts"/>
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/action_settings"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/share_with.xml b/src/main/res/menu/share_with.xml
new file mode 100644
index 00000000..cbd15c11
--- /dev/null
+++ b/src/main/res/menu/share_with.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_add"
+ android:icon="@drawable/ic_action_new"
+ android:orderInCategory="10"
+ android:showAsAction="always"
+ android:title="@string/action_add"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/menu/start_conversation.xml b/src/main/res/menu/start_conversation.xml
new file mode 100644
index 00000000..f7230169
--- /dev/null
+++ b/src/main/res/menu/start_conversation.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_search"
+ android:actionLayout="@layout/actionview_search"
+ android:icon="@drawable/ic_action_search"
+ android:showAsAction="collapseActionView|always"
+ android:title="@string/search"/>
+ <item
+ android:id="@+id/action_create_contact"
+ android:icon="@drawable/ic_action_add_person"
+ android:showAsAction="always"
+ android:title="@string/create_contact"/>
+ <item
+ android:id="@+id/action_join_conference"
+ android:icon="@drawable/ic_action_add_group"
+ android:showAsAction="always"
+ android:title="@string/join_conference"/>
+ <item
+ android:id="@+id/action_accounts"
+ android:orderInCategory="90"
+ android:showAsAction="never"
+ android:title="@string/action_accounts"/>
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/action_settings"/>
+
+</menu> \ No newline at end of file
diff --git a/src/main/res/values-ca/arrays.xml b/src/main/res/values-ca/arrays.xml
new file mode 100644
index 00000000..ff1a0d4b
--- /dev/null
+++ b/src/main/res/values-ca/arrays.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mòbil</item>
+ <item>Telèfon</item>
+ <item>Tauleta</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>mai</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml
new file mode 100644
index 00000000..cfbe428b
--- /dev/null
+++ b/src/main/res/values-ca/strings.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Preferències</string>
+ <string name="action_add">Nova conversa</string>
+ <string name="action_accounts">Gestionar comptes</string>
+ <string name="action_end_conversation">Finalitzar conversa</string>
+ <string name="action_contact_details">Detalls del contacte</string>
+ <string name="action_muc_details">Detalls de la conferència</string>
+ <string name="action_secure">Conversa segura</string>
+ <string name="action_add_account">Afegir compte</string>
+ <string name="just_now">ara</string>
+ <string name="sending">enviant&#8230;</string>
+ <string name="encrypted_message">Desxifrant missatge. Espera si us plau&#8230;</string>
+ <string name="nick_in_use">El sobrenom ja està en ús</string>
+ <string name="moderator">Moderador</string>
+ <string name="participant">Participant</string>
+ <string name="visitor">Visitant</string>
+ <string name="remove_contact_text">Vols eliminar a %s de la teva llista?. La conversa associada a aquest compte no s\'eliminarà.</string>
+ <string name="register_account">Registrar nou compte al servidor</string>
+ <string name="share_with">Compartir amb</string>
+ <string name="start_conversation">Començar conversa</string>
+ <string name="cancel">Cancel·lar</string>
+ <string name="crash_report_title">Conversations s\'ha aturat.</string>
+ <string name="crash_report_message">Enviant bolcats de piles ajudes al desenvolupament de Conversations\n<b>Avís:</b> Això usarà el teu compte XMPP per enviar el bolcat de pila al desenvolupador.</string>
+ <string name="send_now">Enviar ara</string>
+ <string name="send_never">No preguntar de nou</string>
+ <string name="problem_connecting_to_account">No s\'ha pogut connectar al compte</string>
+ <string name="problem_connecting_to_accounts">No s\'ha pogut connectar a múltiples comptes</string>
+ <string name="touch_to_fix">Prem aqui per gestionar els teus comptes</string>
+ <string name="attach_file">Enviar arxiu</string>
+ <string name="not_in_roster">El contacte no està a la teva llista. Vols afegir-lo?</string>
+ <string name="add_contact">Afefgir contacte</string>
+ <string name="send_failed">Error a l\'enviar</string>
+ <string name="send_rejected">rebutjat</string>
+ <string name="receiving_image">Rebent arxiu d\'imatge. Espera si us plau&#8230;</string>
+ <string name="preparing_image">Preparant imatge per enviar</string>
+ <string name="action_clear_history">Netejar historial</string>
+ <string name="clear_conversation_history">Netejar historial de conversa</string>
+ <string name="clear_histor_msg">Vols esborrar tots els missatges d\'aquesta conversa?\n\n<b>Avís:</b> Això no afectarà els missatges desats en altres dispositius o servidors.</string>
+ <string name="delete_messages">Esborrar missatges</string>
+ <string name="also_end_conversation">Finalitzar aquesta conversa més tard</string>
+ <string name="choose_presence">Selecciona recurs del contacte</string>
+ <string name="send_plain_text_message">Enviar missatge de text</string>
+ <string name="send_otr_message">Enviar missatge xifrat amb OTR</string>
+ <string name="send_pgp_message">Enviar missatge xifrat amb OpenPGP</string>
+ <string name="your_nick_has_been_changed">El teu sobrenom s\'ha modificat</string>
+ <string name="download_image">Descarregar imatge</string>
+ <string name="image_offered_for_download"><i>Fitxer d\'imatge ofert per a descàrrega</i></string>
+ <string name="send_unencrypted">Enviar sense xifrar</string>
+ <string name="decryption_failed">Ha fallat el desxiframent. Potser no tinguis la clau privada apropiada.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations utilitza una aplicació de tercers anomenada <b>OpenKeychain</b> per xifrar i desxifrar missatges i gestionar les teves claus públiques..\n\nOpenKeychain està publicat sota llicència GPLv3 i disponible a la F-Droid i Google Play.\n\n<small>(Si us plau, reinicieu Conversations després.)</small></string>
+ <string name="restart">Reiniciar</string>
+ <string name="install">Instal·lar</string>
+ <string name="offering">oferint&#8230;</string>
+ <string name="no_pgp_key">Clau OpenPGP no trobada</string>
+ <string name="contact_has_no_pgp_key">Conversations no ha pogut xifrar els teus missatges perquè el teu contacte no està anunciant la seva clau pública.\n\n<small>Si us plau, demana al teu contacte que configuri OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Missatge xifrat rebut. Prem per desxifrar i veure-ho.</i></string>
+ <string name="encrypted_image_received"><i>Imatge xifrada rebuda. Prem per desxifrar i veure-la.</i></string>
+ <string name="image_file"><i>Imatge rebuda. Prem per veure</i></string>
+ <string name="pref_xmpp_resource">Recursos XMPP</string>
+ <string name="pref_xmpp_resource_summary">El nom que identifica aquest client amb</string>
+ <string name="pref_accept_files">Acceptar fitxers</string>
+ <string name="pref_accept_files_summary">Accepta fitxers automàticament amb una mida menor a&#8230;</string>
+ <string name="pref_notification_settings">Ajustos de notificacions</string>
+ <string name="pref_notifications">Notificacions</string>
+ <string name="pref_notifications_summary">Notifica quan arriba un nou missatge</string>
+ <string name="pref_vibrate">Vibra</string>
+ <string name="pref_vibrate_summary">Vibra quan arriba un nou missatge</string>
+ <string name="pref_sound">So</string>
+ <string name="pref_sound_summary">Reprodueix el to de trucada amb la notificació</string>
+ <string name="pref_conference_notifications">Notificacions de conferència</string>
+ <string name="pref_conference_notifications_summary">Sempre notifica quan arriba un nou missatge de conferència en comptes de només quan està destacat</string>
+ <string name="pref_notification_grace_period">Notificació del període d\'espera</string>
+ <string name="pref_notification_grace_period_summary">Desactiva les notificacions durant un breu termini després de rebre una còpia de missatges carbon</string>
+ <string name="pref_advanced_options">Opcions avançades</string>
+ <string name="pref_never_send_crash">Mai enviïs informes d\'errors</string>
+ <string name="pref_never_send_crash_summary">Enviant traces d\'execució ajudes al futur desenvolupament del Conversations.</string>
+ <string name="pref_ui_options">Opcions de UI</string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-cs/arrays.xml b/src/main/res/values-cs/arrays.xml
new file mode 100644
index 00000000..4510cf84
--- /dev/null
+++ b/src/main/res/values-cs/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mobil</item>
+ <item>Telefon</item>
+ <item>Tablet</item>
+ <item>Konverzace</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>nikdy</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 minut</item>
+ <item>jedna hodina</item>
+ <item>2 hodiny</item>
+ <item>8 hodin</item>
+ <item>než opět změním</item>
+ </string-array>
+
+ <integer-array name="mute_options_durations">
+ <item>1800</item>
+ <item>3600</item>
+ <item>7200</item>
+ <item>28800</item>
+ <item>-1</item>
+ </integer-array>
+
+</resources>
diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml
new file mode 100644
index 00000000..185c5d31
--- /dev/null
+++ b/src/main/res/values-cs/strings.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Nastavení</string>
+ <string name="action_add">Nová konverzace</string>
+ <string name="action_accounts">Nastavení účtů</string>
+ <string name="action_end_conversation">Ukončit tuto konverzaci</string>
+ <string name="action_contact_details">Detaily kontaktu</string>
+ <string name="action_muc_details">Detaily konference</string>
+ <string name="action_secure">Zabezpečená konverzace</string>
+ <string name="action_add_account">Přidat účet</string>
+ <string name="action_edit_contact">Upravit jméno</string>
+ <string name="action_add_phone_book">Přidat do telefonního seznamu</string>
+ <string name="action_delete_contact">Smazat ze seznamu</string>
+ <string name="title_activity_manage_accounts">Nastavení účtů</string>
+ <string name="title_activity_settings">Nastavení</string>
+ <string name="title_activity_conference_details">Detaily konference</string>
+ <string name="title_activity_contact_details">Detaily kontaktu</string>
+ <string name="title_activity_conversations">Konverzace</string>
+ <string name="title_activity_sharewith">Sdílet s konverzací</string>
+ <string name="title_activity_start_conversation">Začít konverzaci</string>
+ <string name="title_activity_choose_contact">Vybrat kontakt</string>
+ <string name="just_now">právě teď</string>
+ <string name="minute_ago">před 1 minutou</string>
+ <string name="minutes_ago">před %d minutami</string>
+ <string name="unread_conversations">nepřečtené konverzace</string>
+ <string name="sending">odesílám&#8230;</string>
+ <string name="encrypted_message">Dešifruji zprávu. Chvíli strpení&#8230;</string>
+ <string name="nick_in_use">Přezdívka se již používá</string>
+ <string name="admin">Administrátor</string>
+ <string name="owner">Vlastník</string>
+ <string name="moderator">Moderátor</string>
+ <string name="participant">Účastník</string>
+ <string name="visitor">Návštěvník</string>
+ <string name="remove_contact_text">Chcete odstranit %s ze svého seznamu? Konverzace spojené s tímto kontaktem nebudou odstraněny.</string>
+ <string name="remove_bookmark_text">Chcete odstranit %s ze záložek? Konverzace spojené s touto záložkou nebudou odstraněny.</string>
+ <string name="register_account">Registrovat nový účet na serveru</string>
+ <string name="share_with">Sdílet s</string>
+ <string name="start_conversation">Začít konverzaci</string>
+ <string name="invite_contact">Pozvat kontakt</string>
+ <string name="contacts">Kontakty</string>
+ <string name="cancel">Zrušit</string>
+ <string name="add">Přidat</string>
+ <string name="edit">Upravit</string>
+ <string name="delete">Smazat</string>
+ <string name="save">Uložit</string>
+ <string name="ok">OK</string>
+ <string name="crash_report_title">Aplikace Konverzace přestala reagovat</string>
+ <string name="crash_report_message">Zasláním detailů o důvodu selhání pomůžete dalšímu vývoji aplikace Konverzace\n<b>Varování:</b> Toto použije nastavený XMPP účet pro zaslání detailů vývojářům.</string>
+ <string name="send_now">Odeslat teď</string>
+ <string name="send_never">Již se neptat</string>
+ <string name="problem_connecting_to_account">Připojení k účtu se nezdařilo</string>
+ <string name="problem_connecting_to_accounts">Připojení k několika účtům se nezdařilo</string>
+ <string name="touch_to_fix">Pro nastavení účtů tapni zde</string>
+ <string name="attach_file">Přiložit soubor</string>
+ <string name="not_in_roster">Kontakt není v seznamu. Chcete ho přidat?</string>
+ <string name="add_contact">Přidat kontakt</string>
+ <string name="send_failed">doručení selhalo</string>
+ <string name="send_rejected">zamítnuto</string>
+ <string name="receiving_image">Přijímám obrázek. Chvíli strpení&#8230;</string>
+ <string name="preparing_image">Připravuji obrázek na přenos</string>
+ <string name="action_clear_history">Smazat historii</string>
+ <string name="clear_conversation_history">Smaže historii konverzací</string>
+ <string name="clear_histor_msg">Chcete smazat všechny zprávy v této konverzaci?\n\n<b>Varování:</b> Toto neovlivní zprávy uložené na jiných přístrojích nebo serverech.</string>
+ <string name="delete_messages">Smazat zprávy</string>
+ <string name="also_end_conversation">Poté ukončit i tuto konverzaci</string>
+ <string name="choose_presence">Vybrat aktualizaci stavu pro kontakt</string>
+ <string name="send_plain_text_message">Poslat textovou zprávu</string>
+ <string name="send_otr_message">Poslat OTR šifrovanou zprávu</string>
+ <string name="send_pgp_message">Poslat OpenPGP šifrovanou zprávu</string>
+ <string name="your_nick_has_been_changed">Přezdívka byla změněna</string>
+ <string name="download_image">Stáhnout obrázek</string>
+ <string name="image_offered_for_download"><i>Byl nabídnut obrázek ke stažení</i></string>
+ <string name="send_unencrypted">Poslat nešifrované</string>
+ <string name="decryption_failed">Zašifrování se nezdařilo. Možná nemáte správný privátní klíč.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Konverzace využívá aplikaci třetí strany, <b>OpenKeychain</b>, k šifrování a dešifrování zpráv a ke správě veřejných klíčů.\n\nOpenKeychain je licencován pod GPLv3 a dostupný na F-Droid a Google Play.\n\n<small>(Po instalaci prosím restartujte aplikaci Konverzace.)</small></string>
+ <string name="restart">Restartovat</string>
+ <string name="install">Instalovat</string>
+ <string name="offering">nabízí&#8230;</string>
+ <string name="waiting">čekám&#8230;</string>
+ <string name="no_pgp_key">Nebyl nalezen žádný OpenPGP klíč</string>
+ <string name="contact_has_no_pgp_key">Není možné zašifrovat zprávu v aplikaci Konverzace, protože druhá strana neoznamuje svůj veřejný klíč.\n\n<small>Požádejte svůj kontakt ať si nastaví OpenPGP.</small></string>
+ <string name="no_pgp_keys">Nebyly nalezeny žádné OpenPGP klíče</string>
+ <string name="contacts_have_no_pgp_keys">Není možné zašifrovat zprávy v aplikaci Konverzace, protože kontakty neoznamují svůj veřejný klíč.\n\n<small>Požádejte své kontakty ať si nastaví OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Byla přijata šifrovaná zpráva. Tapni pro dešifrování a přečtení.</i></string>
+ <string name="encrypted_image_received"><i>Byl přijat šifrovaný obrázek. Tapni pro dešifrování a prohlédnutí.</i></string>
+ <string name="image_file"><i>Byl přijat obrázek. Tapni pro prohlédnutí</i></string>
+ <string name="pref_general">Obecné</string>
+ <string name="pref_xmpp_resource">XMPP zdroj</string>
+ <string name="pref_xmpp_resource_summary">Jméno se kterým se tento klient identifikuje</string>
+ <string name="pref_accept_files">Přijímat soubory</string>
+ <string name="pref_accept_files_summary">Automaticky přijímat soubory menší než&#8230;</string>
+ <string name="pref_notification_settings">Nastavení upozornění</string>
+ <string name="pref_notifications">Upozornění</string>
+ <string name="pref_notifications_summary">Upozornit při přijetí nové zprávy</string>
+ <string name="pref_vibrate">Vibrovat</string>
+ <string name="pref_vibrate_summary">Vibrovat při přijetí nové zprávy</string>
+ <string name="pref_sound">Zvuk</string>
+ <string name="pref_sound_summary">Přehrát zvuk společně s upozorněním</string>
+ <string name="pref_conference_notifications">Upozornění při konferencích</string>
+ <string name="pref_conference_notifications_summary">Vždy upozorňovat při nové konferenční zprávě, nejen pokud je vybrána</string>
+ <string name="pref_notification_grace_period">Četnost upozornění</string>
+ <string name="pref_notification_grace_period_summary">Neupozorňovat krátce poté co byla obdržena kopie zprávy</string>
+ <string name="pref_advanced_options">Pokročilé nastavení</string>
+ <string name="pref_never_send_crash">Neodesílat detaily o pádu aplikace</string>
+ <string name="pref_never_send_crash_summary">Zasláním detailů o důvodu selhání pomůžete dalšímu vývoji aplikace Konverzace</string>
+ <string name="pref_confirm_messages">Potvrzovat zprávy</string>
+ <string name="pref_confirm_messages_summary">Dá vědět kontaktům, že zpráva byla přijata a přečtena</string>
+ <string name="pref_ui_options">Nastavení UI</string>
+ <string name="openpgp_error">OpenKeychain nahlásil chybu</string>
+ <string name="error_decrypting_file">I/O chyba dešifrování souboru</string>
+ <string name="accept">Přijmout</string>
+ <string name="error">Došlo k chybě</string>
+ <string name="pref_grant_presence_updates">Povolit aktualizace stavu</string>
+ <string name="pref_grant_presence_updates_summary">Aktivně povolovat a žádat o zasílání změn stavu pro vytvářené kontakty</string>
+ <string name="subscriptions">Odběry</string>
+ <string name="your_account">Váš účet</string>
+ <string name="keys">Klíče</string>
+ <string name="send_presence_updates">Zasílat změny stavu</string>
+ <string name="receive_presence_updates">Přijímat změny stavu</string>
+ <string name="ask_for_presence_updates">Zažádat o změny stavu</string>
+ <string name="attach_choose_picture">Vybrat obrázek</string>
+ <string name="attach_take_picture">Vyfotit obrázek</string>
+ <string name="preemptively_grant">Aktivně povolovat vyžádání změnu stavu</string>
+ <string name="error_not_an_image_file">Vybraný soubor není obrázek</string>
+ <string name="error_compressing_image">Chyba při konverzi obrázkového souboru</string>
+ <string name="error_file_not_found">Soubor nenalezen</string>
+ <string name="error_io_exception">Obecná I/O chyba. Že by již nebylo volné místo?</string>
+ <string name="error_security_exception_during_image_copy">Aplikace, která byla vybrána pro výběr obrázku, nepovolila přečtení souboru.\n\n<small>Zkuste použít jiného správce souborů pro výběr obrázku</small></string>
+ <string name="account_status_unknown">Neznámý</string>
+ <string name="account_status_disabled">Dočasně vypnuto</string>
+ <string name="account_status_online">Online</string>
+ <string name="account_status_connecting">Připojuji\u2026</string>
+ <string name="account_status_offline">Offline</string>
+ <string name="account_status_unauthorized">Nepřihlášen</string>
+ <string name="account_status_not_found">Server nenalezen</string>
+ <string name="account_status_no_internet">Žádné připojení</string>
+ <string name="account_status_regis_fail">Registrace selhala</string>
+ <string name="account_status_regis_conflict">Uživatelské jméno se již používá</string>
+ <string name="account_status_regis_success">Registrace dokončena</string>
+ <string name="account_status_regis_not_sup">Server nepodporuje registrace</string>
+ <string name="encryption_choice_none">Čistý text</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Upravit účet</string>
+ <string name="mgmt_account_delete">Smazat účet</string>
+ <string name="mgmt_account_disable">Dočasně vypnout</string>
+ <string name="mgmt_account_publish_avatar">Zveřejnit avatar</string>
+ <string name="mgmt_account_publish_pgp">Zveřejnit OpenPGP klíč</string>
+ <string name="mgmt_account_enable">Povolit účet</string>
+ <string name="mgmt_account_are_you_sure">Jste si jisti?</string>
+ <string name="mgmt_account_delete_confirm_text">Pokud smažete svůj účet celá historie konverzací bude ztracena</string>
+ <string name="attach_record_voice">Nahrát hlas</string>
+ <string name="account_settings_jabber_id">Jabber ID</string>
+ <string name="account_settings_password">Heslo</string>
+ <string name="account_settings_example_jabber_id">jmeno@server.cz</string>
+ <string name="account_settings_confirm_password">Potvrdit heslo</string>
+ <string name="password">Heslo</string>
+ <string name="confirm_password">Potvrdit heslo</string>
+ <string name="passwords_do_not_match">Hesla nesouhlasí</string>
+ <string name="invalid_jid">Toto není platné Jabber ID</string>
+ <string name="error_out_of_memory">Nedostatek paměti. Obrázek je příliš velký</string>
+ <string name="add_phone_book_text">Chcete přidat %s do svého telefonního seznamu?</string>
+ <string name="contact_status_online">online</string>
+ <string name="contact_status_free_to_chat">volný pro chat</string>
+ <string name="contact_status_away">pryč</string>
+ <string name="contact_status_extended_away">rozšířené pryč</string>
+ <string name="contact_status_do_not_disturb">nerušit</string>
+ <string name="contact_status_offline">offline</string>
+ <string name="muc_details_conference">Konference</string>
+ <string name="muc_details_other_members">Ostatní členové</string>
+ <string name="server_info_carbon_messages">XEP-0280: Kopie zpráv</string>
+ <string name="server_info_stream_management">XEP-0198: Nastavení proudu</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+ <string name="server_info_available">dostupný</string>
+ <string name="server_info_unavailable">nedostupný</string>
+ <string name="missing_public_keys">Chybí oznámení o veřejném klíči</string>
+ <string name="last_seen_now">právě spatřen</string>
+ <string name="last_seen_min">naposledy spatřen před 1 minutou</string>
+ <string name="last_seen_mins">naposledy spatřen před %d minutami</string>
+ <string name="last_seen_hour">naposledy spatřen před 1 hodinou</string>
+ <string name="last_seen_hours">naposledy spatřen před %d hodinami</string>
+ <string name="last_seen_day">naposledy spatřen před 1 dnem</string>
+ <string name="last_seen_days">naposledy spatřen před %d dny</string>
+ <string name="never_seen">nebyl nikdy spatřen</string>
+ <string name="install_openkeychain">Šifrovaná zpráva. Nainstaluje prosím OpenKeychain pro dešifrování.</string>
+ <string name="unknown_otr_fingerprint">Neznámý OTR identifikátor</string>
+ <string name="openpgp_messages_found">Nalezena OpenPGP šifrovaná zpráva</string>
+ <string name="reception_failed">Příjem selhal</string>
+ <string name="your_fingerprint">Váš identifikátor</string>
+ <string name="otr_fingerprint">OTR identifikátor</string>
+ <string name="verify">Ověřit</string>
+ <string name="decrypt">Dešifrovat</string>
+ <string name="conferences">Konference</string>
+ <string name="search">Hledat</string>
+ <string name="create_contact">Vytvořit kontakt</string>
+ <string name="join_conference">Připojit ke konferenci</string>
+ <string name="delete_contact">Smazat kontakt</string>
+ <string name="view_contact_details">Zobrazit detaily kontaktu</string>
+ <string name="create">Vytvořit</string>
+ <string name="contact_already_exists">Kontakt již existuje</string>
+ <string name="join">Vstoupit</string>
+ <string name="conference_address">Adresa konference</string>
+ <string name="conference_address_example">mistnost@konference.server.cz</string>
+ <string name="save_as_bookmark">Uložit jako záložku</string>
+ <string name="delete_bookmark">Smazat záložku</string>
+ <string name="bookmark_already_exists">Tato záložka již existuje</string>
+ <string name="you">Já</string>
+ <string name="action_edit_subject">Upravit jméno konference</string>
+ <string name="conference_not_found">Konference nenalezena</string>
+ <string name="leave">Odejít</string>
+ <string name="contact_added_you">Kontakt přidán do seznamu</string>
+ <string name="add_back">Opět přidat</string>
+ <string name="contact_has_read_up_to_this_point">%s dočetl až sem</string>
+ <string name="publish">Zveřejnit</string>
+ <string name="touch_to_choose_picture">Tapnout na avatar a vybrat obrázek z galerie</string>
+ <string name="publish_avatar_explanation">Pozor: Každý s povolením vidět změny stavu uvidí tento obrázek.</string>
+ <string name="publishing">Zveřejňuji&#8230;</string>
+ <string name="error_publish_avatar_server_reject">Server odmítl toto zveřejnění</string>
+ <string name="error_publish_avatar_converting">Při konverzi obrázku se něco nezdařilo</string>
+ <string name="error_saving_avatar">Nepodařilo se uložit avatar na disk</string>
+ <string name="or_long_press_for_default">(Stisknout dlouze pro obnovení výchozího stavu)</string>
+ <string name="error_publish_avatar_no_server_support">Váš server nepodporuje zveřejňování avataru</string>
+ <string name="private_message">šeptem</string>
+ <string name="private_message_to">pro %s</string>
+ <string name="send_private_message_to">Zaslat soukromou zprávu pro %s</string>
+ <string name="connect">Připojit</string>
+ <string name="account_already_exists">Tentou účet již existuje</string>
+ <string name="next">Další</string>
+ <string name="server_info_session_established">Současné sezení vytvořeno</string>
+ <string name="additional_information">Dodatečné informace</string>
+ <string name="skip">Přeskočit</string>
+ <string name="disable_notifications">Vypnout upozornění</string>
+ <string name="disable_notifications_for_this_conversation">Vypnout upozornění pro tuto konverzaci</string>
+ <string name="notifications_disabled">Upozornění jsou vypnuta</string>
+ <string name="enable">Povolit</string>
+ <string name="conference_requires_password">Konference vyžaduje heslo</string>
+ <string name="enter_password">Vložit heslo</string>
+ <string name="missing_presence_updates">Kontakt nezasílá informace o změně stavu</string>
+ <string name="request_presence_updates">Nejdříve si prosím vyžádejte povolení o zasílání změn stavu kontatku.\n\n<small>To bude poté použito pro zjištění jakou aplikaci tento kontakt používá.</small></string>
+ <string name="request_now">Ihned vyžádat</string>
+ <string name="delete_fingerprint">Smazat identifikátor</string>
+ <string name="sure_delete_fingerprint">Chcete opravdu smazat tento identifikátor?</string>
+ <string name="ignore">Ignorovat</string>
+ <string name="without_mutual_presence_updates"><b>Varování:</b> Odeslání bez povolení změn stavu může způsobit nečekané problémy na obou stranách.\n\n<small>Přejdi na detaily kontaktu pro ověření povolení o změnách stavu.</small></string>
+ <string name="pref_encryption_settings">Nastavení šifrování</string>
+ <string name="pref_force_encryption">Vynutit šifrování</string>
+ <string name="pref_force_encryption_summary">Vždy zasílat šifrované zprávy (mimo konference)</string>
+ <string name="pref_dont_save_encrypted">Neukládat šifrované zprávy</string>
+ <string name="pref_dont_save_encrypted_summary">Varování: Toto může vést ke ztrátě zpráv</string>
+ <string name="pref_expert_options">Expertní nastavení</string>
+ <string name="pref_expert_options_summary">S tímto zacházejte velmi opatrně</string>
+ <string name="pref_use_larger_font">Zvětšit velikost písma</string>
+ <string name="pref_use_larger_font_summary">Použít větší písmo v celé aplikaci</string>
+ <string name="pref_use_send_button_to_indicate_status">Tlačítko pro odeslání zobrazuje stav</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Obarvit tlačítko pro odeslání barvou stavu kontaktu</string>
+
+</resources>
diff --git a/src/main/res/values-de/arrays.xml b/src/main/res/values-de/arrays.xml
new file mode 100644
index 00000000..9b429c5a
--- /dev/null
+++ b/src/main/res/values-de/arrays.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mobile</item>
+ <item>Phone</item>
+ <item>Tablet</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>nie</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 Minuten</item>
+ <item>eine Stunde</item>
+ <item>2 Stunden</item>
+ <item>8 Stunden</item>
+ <item>bis auf Widerruf</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..72121774
--- /dev/null
+++ b/src/main/res/values-de/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Einstellungen</string>
+ <string name="action_add">Neue Unterhaltung</string>
+ <string name="action_accounts">Konten verwalten</string>
+ <string name="action_end_conversation">Unterhaltung beenden</string>
+ <string name="action_contact_details">Kontaktdetails</string>
+ <string name="action_muc_details">Konferenzdetails</string>
+ <string name="action_secure">Verschlüsselte Unterhaltung</string>
+ <string name="action_add_account">Konto hinzufügen</string>
+ <string name="action_edit_contact">Name bearbeiten</string>
+ <string name="action_add_phone_book">Zum Telefonbuch hinzufügen</string>
+ <string name="action_delete_contact">Aus Kontaktliste entfernen</string>
+ <string name="title_activity_manage_accounts">Konten verwalten</string>
+ <string name="title_activity_settings">Einstellungen</string>
+ <string name="title_activity_conference_details">Konferenzdetails</string>
+ <string name="title_activity_contact_details">Kontaktdetails</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">Mit Unterhaltung teilen</string>
+ <string name="title_activity_start_conversation">Beginne Unterhaltung</string>
+ <string name="title_activity_choose_contact">Kontakt auswählen</string>
+ <string name="just_now">gerade</string>
+ <string name="minute_ago">vor einer Minute</string>
+ <string name="minutes_ago">vor %d Minuten</string>
+ <string name="unread_conversations">ungelesene Unterhaltungen</string>
+ <string name="sending">senden&#8230;</string>
+ <string name="encrypted_message">Entschlüssele Nachricht. Bitte warten&#8230;</string>
+ <string name="nick_in_use">Nickname wird bereits verwendet</string>
+ <string name="admin">Administrator</string>
+ <string name="owner">Eigentümer</string>
+ <string name="moderator">Moderator</string>
+ <string name="participant">Teilnehmer</string>
+ <string name="visitor">Besucher</string>
+ <string name="remove_contact_text">Möchtest du %s von deiner Kontaktliste entfernen? Die Unterhaltung mit diesem Kontakt wird dabei nicht entfernt.</string>
+ <string name="remove_bookmark_text">Möchtest du das Lesezeichen %s entfernen? Die Unterhaltung mit diesem Lesezeichen wird dabei nicht entfernt.</string>
+ <string name="register_account">Neues Konto auf dem Server erstellen</string>
+ <string name="share_with">Teile mit&#8230;</string>
+ <string name="start_conversation">Beginne Unterhaltung</string>
+ <string name="invite_contact">Kontakt einladen</string>
+ <string name="contacts">Kontakte</string>
+ <string name="cancel">Abbrechen</string>
+ <string name="add">Hinzufügen</string>
+ <string name="edit">Bearbeiten</string>
+ <string name="delete">Entfernen</string>
+ <string name="save">Speichern</string>
+ <string name="ok">OK</string>
+ <string name="crash_report_title">Conversations ist abgestürzt</string>
+ <string name="crash_report_message">Durch das Einsenden von Fehlerberichten hilfst du bei der stetigen Verbesserung von Conversations.\n<b>Achtung:</b> Dies wird eines deiner XMPP-Konten benutzen, um den Entwickler zu kontaktieren.</string>
+ <string name="send_now">Jetzt abschicken</string>
+ <string name="send_never">Nie mehr nachfragen</string>
+ <string name="problem_connecting_to_account">Es gibt Probleme beim Verbindungsaufbau mit einem Konto</string>
+ <string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konto</string>
+ <string name="touch_to_fix">Drücke hier, um das Konto zu verwalten</string>
+ <string name="attach_file">Datei anfügen</string>
+ <string name="not_in_roster">Der Kontakt ist nicht in deiner Kontaktliste. Möchtest du ihn hinzufügen?</string>
+ <string name="add_contact">Kontakt hinzufügen</string>
+ <string name="send_failed">Zustellung nicht erfolgreich</string>
+ <string name="send_rejected">abgelehnt</string>
+ <string name="receiving_image">Empfange Bild. Bitte warten&#8230;</string>
+ <string name="preparing_image">Bereite Bild für die Übertragung vor</string>
+ <string name="action_clear_history">Verlauf löschen</string>
+ <string name="clear_conversation_history">Unterhaltungsverlauf löschen</string>
+ <string name="clear_histor_msg">Möchtest du alle Nachrichten in dieser Unterhaltung löschen?\n\n<b>Achtung:</b> Dies beeinflusst nicht Nachrichten, die auf anderen Geräten oder Servern gespeichert sind.</string>
+ <string name="delete_messages">Nachrichten löschen</string>
+ <string name="also_end_conversation">Diese Unterhaltung danach beenden</string>
+ <string name="choose_presence">Choose presence to contact</string>
+ <string name="send_plain_text_message">Unverschlüsselt schreiben</string>
+ <string name="send_otr_message">OTR-verschlüsselt schreiben</string>
+ <string name="send_pgp_message">OpenPGP-verschlüsselt schreiben</string>
+ <string name="your_nick_has_been_changed">Dein Nickname wurde geändert</string>
+ <string name="download_image">Bild herunterladen</string>
+ <string name="image_offered_for_download"><i>Bilddatei zum Download angeboten</i></string>
+ <string name="send_unencrypted">Unverschlüsselt verschicken</string>
+ <string name="decryption_failed">Entschlüsselung fehlgeschlagen. Vielleicht hast du nicht den richtigen privaten Schlüssel.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations benutzt eine Drittanwendung namens <b>OpenKeychain</b>, um Nachrichten zu ver- und entschlüsseln und um deine Schlüssel zu verwalten.\n\nOpenKeychain ist GPLv3-lizenziert und kann über F-Droid oder Google Play bezogen werden.\n\n<small>(Bitte starte Conversations danach neu.)</small></string>
+ <string name="restart">Neustarten</string>
+ <string name="install">Installieren</string>
+ <string name="offering">angeboten&#8230;</string>
+ <string name="waiting">warten&#8230;</string>
+ <string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</string>
+ <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
+ <string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string>
+ <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
+ <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie anzuzeigen und zu entschlüsseln.</i></string>
+ <string name="encrypted_image_received"><i>Verschlüsseltes Bild erhalten. Drücke hier, um es anzuzeigen und zu entschlüsseln.</i></string>
+ <string name="image_file"><i>Bild erhalten. Drücke hier, um es anzuzeigen.</i></string>
+ <string name="pref_general">Allgemein</string>
+ <string name="pref_xmpp_resource">XMPP-Ressource</string>
+ <string name="pref_xmpp_resource_summary">Der Name, mit dem sich der Client selbst identifiziert</string>
+ <string name="pref_accept_files">Dateiannahme</string>
+ <string name="pref_accept_files_summary">Dateien, die kleiner sind als &#8230;, automatisch annehmen</string>
+ <string name="pref_notification_settings">Benachrichtigungseinstellungen</string>
+ <string name="pref_notifications">Benachrichtigungen</string>
+ <string name="pref_notifications_summary">Benachrichtige mich, wenn eine neue Nachricht ankommt</string>
+ <string name="pref_vibrate">Vibrieren</string>
+ <string name="pref_vibrate_summary">Vibriere, wenn eine neue Nachricht ankommt</string>
+ <string name="pref_sound">Klingelton</string>
+ <string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string>
+ <string name="pref_conference_notifications">Konferenz-Benachrichtigungen</string>
+ <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde.</string>
+ <string name="pref_notification_grace_period">Gnadenfrist</string>
+ <string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string>
+ <string name="pref_advanced_options">Erweiterte Optionen</string>
+ <string name="pref_never_send_crash">Sende niemals Absturzberichte</string>
+ <string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string>
+ <string name="pref_confirm_messages">Lesebestätigung senden</string>
+ <string name="pref_confirm_messages_summary">Informiere deine Kontakte, wenn du eine Nachricht empfängst oder liest</string>
+ <string name="openpgp_error">Fehler mit OpenKeychain</string>
+ <string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string>
+ <string name="accept">Annehmen</string>
+ <string name="error">Ein unbekannter Fehler ist aufgetreten</string>
+ <string name="pref_grant_presence_updates">Online-Status</string>
+ <string name="pref_grant_presence_updates_summary">Erlaube Kontakten, die von dir erstellt wurden, deinen Status zu sehen und frage um Erlaubnis, ihren sehen zu dürfen</string>
+ <string name="subscriptions">Abonnements</string>
+ <string name="your_account">Dein Konto</string>
+ <string name="keys">Schlüssel</string>
+ <string name="send_presence_updates">Anwesenheitsbenachrichtigungen senden</string>
+ <string name="receive_presence_updates">Empfange Anwesenheitsbenachrichtigungen</string>
+ <string name="ask_for_presence_updates">Frage um Erlaubnis, Anwesenheitsbenachrichtigungen sehen zu dürfen</string>
+ <string name="attach_choose_picture">Foto auswählen</string>
+ <string name="attach_take_picture">Foto aufnehmen</string>
+ <string name="preemptively_grant">Erlaube Statusanfrage vorab</string>
+ <string name="error_not_an_image_file">Die ausgewählte Datei ist kein Bild</string>
+ <string name="error_compressing_image">Fehler beim Umwandeln des Bildes</string>
+ <string name="error_file_not_found">Datei nicht gefunden</string>
+ <string name="error_io_exception">Allgemeiner Fehler. Vielleicht hast du keinen Speicherplatz mehr?</string>
+ <string name="error_security_exception_during_image_copy">Die App, mit der du das Bild ausgesucht hast, hat uns keine Rechte eingeräumt, das Bild zu betrachten.\n\n<small>Benutze einen anderen Dateimanager</small></string>
+ <string name="account_status_unknown">Unbekannt</string>
+ <string name="account_status_disabled">Vorübergehend abgeschaltet</string>
+ <string name="account_status_online">Online</string>
+ <string name="account_status_connecting">Verbinde\u2026</string>
+ <string name="account_status_offline">Offline</string>
+ <string name="account_status_unauthorized">Ungültige Zugangsdaten</string>
+ <string name="account_status_not_found">Server nicht gefunden</string>
+ <string name="account_status_no_internet">Keine Internetverbindung</string>
+ <string name="account_status_regis_fail">Registrierung fehlgeschlagen</string>
+ <string name="account_status_regis_conflict">Benutzername wird bereits verwendet</string>
+ <string name="account_status_regis_success">Registrierung abgeschlossen</string>
+ <string name="account_status_regis_not_sup">Der Server unterstützt keine Registrierung</string>
+ <string name="encryption_choice_none">Klartext</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Konto bearbeiten</string>
+ <string name="mgmt_account_delete">Löschen</string>
+ <string name="mgmt_account_disable">Vorübergehend abschalten</string>
+ <string name="mgmt_account_publish_avatar">Avatar veröffentlichen</string>
+ <string name="mgmt_account_publish_pgp">Öffentlichen OpenPGP-Schlüssel veröffentlichen</string>
+ <string name="mgmt_account_enable">Anschalten</string>
+ <string name="mgmt_account_are_you_sure">Bist du dir sicher?</string>
+ <string name="mgmt_account_delete_confirm_text">Wenn du dein Konto löscht, gehen alle Gesprächsverläufe verloren</string>
+ <string name="attach_record_voice">Sprache aufzeichnen</string>
+ <string name="account_settings_jabber_id">Jabber-ID:</string>
+ <string name="account_settings_password">Passwort:</string>
+ <string name="account_settings_example_jabber_id">benutzer@domain.de</string>
+ <string name="account_settings_confirm_password">Passwort bestätigen</string>
+ <string name="password">Passwort</string>
+ <string name="confirm_password">Passwort bestätigen</string>
+ <string name="passwords_do_not_match">Passwörter stimmen nicht überein</string>
+ <string name="invalid_jid">Ungültige Jabber-ID</string>
+ <string name="error_out_of_memory">Zu wenig Speicher vorhanden. Das Bild ist zu groß</string>
+ <string name="add_phone_book_text">Möchtest du %s zum Telefonbuch hinzufügen?</string>
+ <string name="contact_status_online">Online</string>
+ <string name="contact_status_free_to_chat">Bereit</string>
+ <string name="contact_status_away">Abwesend</string>
+ <string name="contact_status_extended_away">Abwesend (erweitert)</string>
+ <string name="contact_status_do_not_disturb">Nicht stören</string>
+ <string name="contact_status_offline">Offline</string>
+ <string name="muc_details_conference">Konferenz</string>
+ <string name="muc_details_other_members">Andere Mitglieder</string>
+ <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+ <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatare)</string>
+ <string name="server_info_available">verfügbar</string>
+ <string name="server_info_unavailable">nicht verfügbar</string>
+ <string name="missing_public_keys">Öffentlicher Schlüssel fehlt</string>
+ <string name="last_seen_now">Gerade online</string>
+ <string name="last_seen_min">Vor einer Minute gesehen</string>
+ <string name="last_seen_mins">Vor %d Minuten gesehen</string>
+ <string name="last_seen_hour">Vor einer Stunde gesehen</string>
+ <string name="last_seen_hours">Vor %d Stunden gesehen</string>
+ <string name="last_seen_day">Vor einem Tag gesehen</string>
+ <string name="last_seen_days">Vor %d Tagen gesehen</string>
+ <string name="never_seen">Noch nie gesehen</string>
+ <string name="install_openkeychain">Verschlüsselte Nachricht. Bitte installiere OpenKeychain zur Entschlüsselung.</string>
+ <string name="unknown_otr_fingerprint">Unbekannter OTR-Fingerabdruck</string>
+ <string name="openpgp_messages_found">Verschlüsselte OpenPGP-Nachricht gefunden</string>
+ <string name="reception_failed">Empfang ist fehlgeschlagen</string>
+ <string name="your_fingerprint">Dein Fingerabdruck</string>
+ <string name="otr_fingerprint">OTR-Fingerabdruck</string>
+ <string name="verify">Verifizieren</string>
+ <string name="decrypt">Entschlüsseln</string>
+ <string name="conferences">Konferenzen</string>
+ <string name="search">Suche</string>
+ <string name="create_contact">Kontakt erstellen</string>
+ <string name="join_conference">Konferenz beitreten</string>
+ <string name="delete_contact">Kontakt löschen</string>
+ <string name="view_contact_details">Kontaktdetails anzeigen</string>
+ <string name="create">Erstellen</string>
+ <string name="contact_already_exists">Der Kontakt existiert bereits</string>
+ <string name="join">Beitreten</string>
+ <string name="conference_address">Konferenzadresse</string>
+ <string name="conference_address_example">raum@conference.example.com</string>
+ <string name="save_as_bookmark">Als Lesezeichen speichern</string>
+ <string name="delete_bookmark">Lesezeichen löschen</string>
+ <string name="bookmark_already_exists">Das Lesezeichen existiert bereits</string>
+ <string name="you">Du</string>
+ <string name="action_edit_subject">Konferenzthema anpassen</string>
+ <string name="conference_not_found">Konferenz nicht gefunden</string>
+ <string name="leave">Verlassen</string>
+ <string name="contact_added_you">Der Kontakt hat dich zur Kontaktliste hinzugefügt</string>
+ <string name="add_back">Auch hinzufügen</string>
+ <string name="contact_has_read_up_to_this_point">%s hat bis zu diesem Punkt gelesen</string>
+ <string name="publish">Veröffentlichen</string>
+ <string name="touch_to_choose_picture">Klicke hier, um einen Avatar auszuwählen</string>
+ <string name="publish_avatar_explanation">Achtung: Jeder, der deinen Status sehen darf, sieht auch deinen Avatar.</string>
+ <string name="publishing">Veröffentliche&#8230;</string>
+ <string name="error_publish_avatar_server_reject">Der Server hat die Veröffentlichung des Avatars abgelehnt.</string>
+ <string name="error_publish_avatar_converting">Bei der Konvertierung des Avatars lief etwas schief.</string>
+ <string name="error_saving_avatar">Kann Avatar nicht speichern.</string>
+ <string name="or_long_press_for_default">(Oder klicke lange, um Standard wiederherzustellen)</string>
+ <string name="error_publish_avatar_no_server_support">Dein Server unterstützt die Veröffentlichung von Avataren nicht.</string>
+ <string name="private_message">private Nachricht</string>
+ <string name="private_message_to">an %s</string>
+ <string name="send_private_message_to">Sende private Nachricht an %s</string>
+ <string name="connect">Verbinden</string>
+ <string name="account_already_exists">Das Konto existiert bereits</string>
+ <string name="next">Weiter</string>
+ <string name="server_info_session_established">Aktuelle Sitzung wiederhergestellt</string>
+ <string name="additional_information">Zusätzliche Informationen</string>
+ <string name="skip">Überspringen</string>
+ <string name="pref_ui_options">Benutzeroberfläche</string>
+ <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string>
+ <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte das dies nicht unbedingt in allen Fällen funktioniert.</string>
+ <string name="disable_notifications">Benachrichtigungen deaktivieren</string>
+ <string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string>
+ <string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string>
+ <string name="enable">Aktivieren</string>
+ <string name="conference_requires_password">Konferenz ist passwortgeschützt</string>
+ <string name="enter_password">Passwort eingeben</string>
+ <string name="missing_presence_updates">Fehlender Online-Status vom Kontakt</string>
+ <string name="request_presence_updates">Bitte erst Anwesenheitsbenachrichtigungen vom Kontakt anfordern.\n\n</string>
+ <string name="request_now">Jetzt anfordern</string>
+ <string name="delete_fingerprint">Fingerabdruck löschen</string>
+ <string name="sure_delete_fingerprint">Soll dieser Fingerabdruck definitiv gelöscht werden?</string>
+ <string name="ignore">Ignorieren</string>
+ <string name="without_mutual_presence_updates"><b>Achtung:</b> Es kann zu unerwarteten Problemen führen, dies ohne gegenseitige Anwesenheitsbenachrichtigungen abzusenden.\n\n<small>Bitte die Online-Status-Abonnements in den Kontaktdetails prüfen.</small></string>
+ <string name="pref_encryption_settings">Verschlüsselungs-Einstellungen</string>
+ <string name="pref_force_encryption">Ende-zu-Ende-Verschlüsselung forcieren</string>
+ <string name="pref_force_encryption_summary">Nachrichten immer verschlüsseln (außer für Konferenzen)</string>
+ <string name="pref_dont_save_encrypted">Verschlüsselte Nachrichten nicht speichern</string>
+ <string name="pref_dont_save_encrypted_summary">Achtung: Kann zu Nachrichtenverlust führen</string>
+ <string name="pref_expert_options">Einstellungen für Experten</string>
+ <string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string>
+ <string name="pref_use_larger_font">Schriftgröße erhöhen</string>
+ <string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string>
+ <string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string>
+ <string name="pref_expert_options_other">Sonstiges</string>
+ <string name="pref_conference_name">Konferenz-Name</string>
+ <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Name verwenden</string>
+ <string name="toast_message_otr_fingerprint">OTR Fingerabdruck in die Zwischenablage kopiert!</string>
+ <string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string>
+ <string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string>
+ <string name="conference_kicked">Du wurdest aus dem Konferenzraum geworfen</string>
+
+</resources>
diff --git a/src/main/res/values-es/arrays.xml b/src/main/res/values-es/arrays.xml
new file mode 100644
index 00000000..15231955
--- /dev/null
+++ b/src/main/res/values-es/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Móvil</item>
+ <item>Teléfono</item>
+ <item>Tablet</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>nunca</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 minutos</item>
+ <item>1 hora</item>
+ <item>2 horas</item>
+ <item>8 horas</item>
+ <item>Hasta nuevo aviso</item>
+ </string-array>
+
+ <integer-array name="mute_options_durations">
+ <item>1800</item>
+ <item>3600</item>
+ <item>7200</item>
+ <item>28800</item>
+ <item>-1</item>
+ </integer-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..7fdc95c0
--- /dev/null
+++ b/src/main/res/values-es/strings.xml
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Ajustes</string>
+ <string name="action_add">Nueva conversación</string>
+ <string name="action_accounts">Gestionar cuentas</string>
+ <string name="action_end_conversation">Terminar conversación</string>
+ <string name="action_contact_details">Detalles del contacto</string>
+ <string name="action_muc_details">Detalles de la conferencia</string>
+ <string name="action_secure">Conversación segura</string>
+ <string name="action_add_account">Añadir cuenta</string>
+ <string name="action_edit_contact">Editar contacto</string>
+ <string name="action_delete_contact">Eliminar contacto de la lista</string>
+ <string name="action_add_phone_book">Añadir a contactos del teléfono</string>
+ <string name="title_activity_manage_accounts">Gestionar Cuentas</string>
+ <string name="title_activity_settings">Ajustes</string>
+ <string name="title_activity_conference_details">Detalles de Conferencia</string>
+ <string name="title_activity_contact_details">Detalles del Contacto</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">Compartir con Conversación</string>
+ <string name="title_activity_start_conversation">Nueva Conversación</string>
+ <string name="title_activity_choose_contact">Elegir Contacto</string>
+ <string name="just_now">ahora</string>
+ <string name="minute_ago">hace 1 min</string>
+ <string name="minutes_ago">hace %d min</string>
+ <string name="unread_conversations">conversaciones por leer</string>
+ <string name="sending">enviando&#8230;</string>
+ <string name="encrypted_message">Desencriptando mensaje. Espera por favor&#8230;</string>
+ <string name="nick_in_use">El apodo ya está en uso</string>
+ <string name="admin">Administrador</string>
+ <string name="owner">Propietario</string>
+ <string name="moderator">Moderador</string>
+ <string name="participant">Participante</string>
+ <string name="visitor">Visitante</string>
+ <string name="remove_contact_text">¿Quieres eliminar a %s de tu lista? La conversación asociada a esta cuenta no se eliminará.</string>
+ <string name="remove_bookmark_text">¿Quieres eliminar %s de tus marcadores? La conversación de la conferencia asociada con este marcador no se eliminará.</string>
+ <string name="register_account">Registrar nueva cuenta en servidor</string>
+ <string name="share_with">Compartir con</string>
+ <string name="start_conversation">Comenzar conversación</string>
+ <string name="invite_contact">Invitar contactos</string>
+ <string name="contacts">Contactos</string>
+ <string name="cancel">Cancelar</string>
+ <string name="add">Añadir</string>
+ <string name="edit">Editar</string>
+ <string name="delete">Eliminar</string>
+ <string name="save">Guardar</string>
+ <string name="ok">OK</string>
+ <string name="crash_report_title">Conversations se ha detenido.</string>
+ <string name="crash_report_message">Si envías un informe de fallos ayudas al desarrollo de Conversations\n<b>Aviso:</b> Esto usará tu cuenta XMPP para enviar los registros de error al desarrollador.</string>
+ <string name="send_now">Enviar ahora</string>
+ <string name="send_never">No preguntar de nuevo</string>
+ <string name="problem_connecting_to_account">No se ha podido conectar a la cuenta</string>
+ <string name="problem_connecting_to_accounts">No se ha podido conectar a múltiples cuentas</string>
+ <string name="touch_to_fix">Pulsa aquí para gestionar tus cuentas</string>
+ <string name="attach_file">Adjuntar</string>
+ <string name="not_in_roster">El contacto no está en tu lista. ¿Te gustaría añadirlo?</string>
+ <string name="add_contact">Añadir contacto</string>
+ <string name="send_failed">Error al enviar</string>
+ <string name="send_rejected">rechazado</string>
+ <string name="receiving_image">Recibiendo archivo de imagen. Espera por favor&#8230;</string>
+ <string name="preparing_image">Preparando imagen para enviar</string>
+ <string name="action_clear_history">Limpiar historial</string>
+ <string name="clear_conversation_history">Limpiar historial de conversación</string>
+ <string name="clear_histor_msg">¿Quieres borrar todos los mensajes de esta conversación?\n\n<b>Aviso:</b> Esto no afectará a los mensajes guardados en otros dispositivos o servidores.</string>
+ <string name="delete_messages">Borrar mensajes</string>
+ <string name="also_end_conversation">Terminar esta conversación más tarde</string>
+ <string name="choose_presence">Selecciona recurso del contacto</string>
+ <string name="send_plain_text_message">Enviar mensaje de texto</string>
+ <string name="send_otr_message">Enviar mensaje encriptado con OTR</string>
+ <string name="send_pgp_message">Enviar mensaje encriptado con OpenPGP</string>
+ <string name="your_nick_has_been_changed">Tu apodo se ha modificado</string>
+ <string name="download_image">Descargar imagen</string>
+ <string name="image_offered_for_download"><i>Archivo de imagen ofrecido para descarga</i></string>
+ <string name="send_unencrypted">Enviar sin encriptar</string>
+ <string name="decryption_failed">Falló la desencriptación. Tal vez no tengas la clave privada apropiada.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations utiliza una aplicación de terceros llamada <b>OpenKeychain</b> para encriptar y desencriptar mensajes y gestionar tus claves públicas.\n\nOpenKeychain está publicado bajo licencia GPLv3 y disponible on F-Droid y Google Play.\n\n<small>(Por favor, reinicie Conversations después.)</small></string>
+ <string name="restart">Reiniciar</string>
+ <string name="install">Instalar</string>
+ <string name="offering">ofreciendo&#8230;</string>
+ <string name="waiting">esperando&#8230;</string>
+ <string name="no_pgp_key">Clave OpenPGP no encontrada</string>
+ <string name="contact_has_no_pgp_key">Conversations no ha podido encriptar tus mensajes porque el contacto no está anunciando su clave publica.\n\n<small>Por favor, pide a tu contacto que configure OpenPGP.</small></string>
+ <string name="no_pgp_keys">Claves OpenPGP no encontradas</string>
+ <string name="contacts_have_no_pgp_keys">Conversations no ha podido encriptar tus mensajes porque tus contactos no están anunciando su clave publica.\n\n<small>Por favor, pide a tus contactos que configuren OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Mensaje encriptado recibido. Pulsa para ver.</i></string>
+ <string name="encrypted_image_received"><i>Imagen encriptada recibida. Pulsa para ver.</i></string>
+ <string name="image_file"><i>Imagen recibida. Pulsa para ver</i></string>
+ <string name="pref_general">General</string>
+ <string name="pref_xmpp_resource">Recurso</string>
+ <string name="pref_xmpp_resource_summary">El nombre que identifica el cliente que estás utilizando</string>
+ <string name="pref_accept_files">Aceptar archivos</string>
+ <string name="pref_accept_files_summary">De forma automática aceptar archivos menores que&#8230;</string>
+ <string name="pref_notification_settings">Ajustes de notificación</string>
+ <string name="pref_notifications">Notificaciones</string>
+ <string name="pref_notifications_summary">Notifica cuando llega un nuevo mensaje</string>
+ <string name="pref_vibrate">Vibrar</string>
+ <string name="pref_vibrate_summary">Vibra cuando llega un nuevo mensaje</string>
+ <string name="pref_sound">Sonido</string>
+ <string name="pref_sound_summary">Reproduce tono con la notificación</string>
+ <string name="pref_conference_notifications">Notificaciones de conferencia</string>
+ <string name="pref_conference_notifications_summary">Siempre notifica cuando llega un mensaje de conferencia y no solo cuando llega un mensaje destacado</string>
+ <string name="pref_notification_grace_period">Notificaciones Carbons</string>
+ <string name="pref_notification_grace_period_summary">Deshabilita las notificaciones durante un corto periodo de tiempo después de recibir la copia del mensaje carbon</string>
+ <string name="pref_advanced_options">Opciones avanzadas</string>
+ <string name="pref_never_send_crash">Nunca enviar informe de fallos</string>
+ <string name="pref_never_send_crash_summary">Si envías registros de error ayudas al desarrollo de Conversations</string>
+ <string name="pref_confirm_messages">Confirmar Mensajes</string>
+ <string name="pref_confirm_messages_summary">Permitir a tus contactos saber cuando recibes y lees un mensaje</string>
+ <string name="pref_ui_options">Opciones de interfaz</string>
+ <string name="openpgp_error">OpenKeychain reportó un error</string>
+ <string name="error_decrypting_file">Error desencriptando fichero</string>
+ <string name="accept">Aceptar</string>
+ <string name="error">Ha ocurrido un error</string>
+ <string name="pref_grant_presence_updates">Suscripción de presencia</string>
+ <string name="pref_grant_presence_updates_summary">De forma automática solicitar y conceder suscripciones de presencia de los contactos que has creado</string>
+ <string name="subscriptions">Suscripciones</string>
+ <string name="your_account">Tu cuenta</string>
+ <string name="keys">Claves</string>
+ <string name="send_presence_updates">Enviar actualizaciones de presencia</string>
+ <string name="receive_presence_updates">Recibir actualizaciones de presencia</string>
+ <string name="ask_for_presence_updates">Solicitar actualizaciones de presencia</string>
+ <string name="attach_choose_picture">Seleccionar imagen</string>
+ <string name="attach_take_picture">Hacer foto</string>
+ <string name="preemptively_grant">De forma automática conceder solicitud de suscripción</string>
+ <string name="error_not_an_image_file">El archivo seleccionado no es una imagen</string>
+ <string name="error_compressing_image">Error comprimiendo el archivo de imagen</string>
+ <string name="error_file_not_found">Archivo no encontrado</string>
+ <string name="error_io_exception">Error general. ¿Puede que no tengas espacio en disco?</string>
+ <string name="error_security_exception_during_image_copy">La aplicación que usas para seleccionar imágenes no proporciona suficientes permisos para leer el archivo.\n\n<small>Utiliza un explorador de ficheros diferente para seleccionar la imagen</small></string>
+ <string name="account_status_unknown">Desconocido</string>
+ <string name="account_status_disabled">Deshabilitado temporalmente</string>
+ <string name="account_status_online">Conectado</string>
+ <string name="account_status_connecting">Conectando\u2026</string>
+ <string name="account_status_offline">Desconectado</string>
+ <string name="account_status_unauthorized">No autorizado</string>
+ <string name="account_status_not_found">Servidor no encontrado</string>
+ <string name="account_status_no_internet">Sin conectividad</string>
+ <string name="account_status_regis_fail">Error en el registro</string>
+ <string name="account_status_regis_conflict">El identificador ya está en uso</string>
+ <string name="account_status_regis_success">Registro completado</string>
+ <string name="account_status_regis_not_sup">El servidor no soporta registros</string>
+ <string name="encryption_choice_none">Texto plano</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Editar cuenta</string>
+ <string name="mgmt_account_delete">Eliminar cuenta</string>
+ <string name="mgmt_account_disable">Deshabilitar temporalmente</string>
+ <string name="mgmt_account_publish_avatar">Imagen de perfil</string>
+ <string name="mgmt_account_publish_pgp">Publicar clave pública OpenPGP</string>
+ <string name="mgmt_account_enable">Habilitar</string>
+ <string name="mgmt_account_are_you_sure">¿Estás seguro?</string>
+ <string name="mgmt_account_delete_confirm_text">Si eliminas tu cuenta tu historial completo de conversaciones se perderá</string>
+ <string name="attach_record_voice">Grabar audio</string>
+ <string name="account_settings_jabber_id">Identificador Jabber</string>
+ <string name="account_settings_password">Contraseña</string>
+ <string name="account_settings_example_jabber_id">usuario@ejemplo.com</string>
+ <string name="account_settings_confirm_password">Confirmar contraseña</string>
+ <string name="password">Contraseña</string>
+ <string name="confirm_password">Confirmar contraseña</string>
+ <string name="passwords_do_not_match">Las contraseñas no coinciden</string>
+ <string name="invalid_jid">El identificador no es un identificador de Jabber válido</string>
+ <string name="error_out_of_memory">Sin memoria. La imagen es demasiado grande</string>
+ <string name="add_phone_book_text">¿Te gustaría añadir a %s a tus contactos del teléfono?</string>
+ <string name="contact_status_online">Disponible</string>
+ <string name="contact_status_free_to_chat">Hablador</string>
+ <string name="contact_status_away">Ausente</string>
+ <string name="contact_status_extended_away">Ausencia extendida</string>
+ <string name="contact_status_do_not_disturb">No molestar</string>
+ <string name="contact_status_offline">Desconectado</string>
+ <string name="muc_details_conference">Conferencia</string>
+ <string name="muc_details_other_members">Otros Miembros</string>
+ <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+ <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+ <string name="server_info_available">Sí</string>
+ <string name="server_info_unavailable">No</string>
+ <string name="missing_public_keys">Se han perdido las claves de anuncio públicas</string>
+ <string name="last_seen_now">Visto última vez ahora</string>
+ <string name="last_seen_min">Visto última vez hace 1 minuto</string>
+ <string name="last_seen_mins">Visto última vez hace %d minutos</string>
+ <string name="last_seen_hour">Visto última vez hace 1 hora</string>
+ <string name="last_seen_hours">Visto última vez hace %d horas</string>
+ <string name="last_seen_day">Visto última vez hace 1 día</string>
+ <string name="last_seen_days">Visto última vez hace %d días</string>
+ <string name="never_seen">Nunca visto</string>
+ <string name="install_openkeychain">Mensaje encriptado. Por favor instala OpenKeychain para desencriptar.</string>
+ <string name="unknown_otr_fingerprint">Clave OTR desconocida</string>
+ <string name="openpgp_messages_found">Encontrado mensaje encriptado con OpenPGP</string>
+ <string name="reception_failed">Error al recibir</string>
+ <string name="your_fingerprint">Tu clave</string>
+ <string name="otr_fingerprint">Clave OTR</string>
+ <string name="verify">Verificar</string>
+ <string name="decrypt">Desencriptar</string>
+ <string name="conferences">Conferencias</string>
+ <string name="search">Buscar</string>
+ <string name="create_contact">Crear Contacto</string>
+ <string name="join_conference">Unirse a Conferencia</string>
+ <string name="delete_contact">Eliminar Contacto</string>
+ <string name="view_contact_details">Ver detalles del contacto</string>
+ <string name="create">Crear</string>
+ <string name="contact_already_exists">El contacto ya existe</string>
+ <string name="join">Unirse</string>
+ <string name="conference_address">Dirección de la Conferencia</string>
+ <string name="conference_address_example">nombre@conferencia.ejemplo.com</string>
+ <string name="save_as_bookmark">Guardar en marcadores</string>
+ <string name="delete_bookmark">Eliminar marcador</string>
+ <string name="bookmark_already_exists">Este marcador ya exsite</string>
+ <string name="you">Tú</string>
+ <string name="action_edit_subject">Editar asunto de la conferencia</string>
+ <string name="conference_not_found">Conferencia no encontrada</string>
+ <string name="leave">Salir</string>
+ <string name="contact_added_you">El contacto te ha añadido a su lista de contactos</string>
+ <string name="add_back">Añadir contacto</string>
+ <string name="contact_has_read_up_to_this_point">%s ha leído hasta aquí</string>
+ <string name="publish">Publicar</string>
+ <string name="touch_to_choose_picture">Pulsa para seleccionar una imagen de la galería</string>
+ <string name="publish_avatar_explanation">Nota: Todos tus contactos podrán ver esta imagen.</string>
+ <string name="publishing">Publicando&#8230;</string>
+ <string name="error_publish_avatar_server_reject">El servidor rechazó la publicación</string>
+ <string name="error_publish_avatar_converting">Se ha producido un error mientras se convertía la imagen</string>
+ <string name="error_saving_avatar">No se ha podido guardar la imagen de perfil en disco</string>
+ <string name="or_long_press_for_default">(O pulsación prolongada para volver a tu imagen de la agenda)</string>
+ <string name="error_publish_avatar_no_server_support">Tu servidor no soporta la publicación de imágenes de perfil</string>
+ <string name="private_message">en privado</string>
+ <string name="private_message_to">en privado para %s</string>
+ <string name="send_private_message_to">Enviar mensaje privado a %s</string>
+ <string name="connect">Conectar</string>
+ <string name="account_already_exists">Esta cuenta ya existe</string>
+ <string name="next">Siguiente</string>
+ <string name="server_info_session_established">Inicio sesión actual</string>
+ <string name="additional_information">Información adicional</string>
+ <string name="skip">Omitir</string>
+ <string name="disable_notifications">Deshabilitar notificaciones</string>
+ <string name="disable_notifications_for_this_conversation">Deshabilitar notificaciones para esta conversación</string>
+ <string name="notifications_disabled">Las notificaciones están deshabilitadas</string>
+ <string name="enable">Habilitar</string>
+ <string name="conference_requires_password">La conferencia requiere contraseña</string>
+ <string name="enter_password">Introduce la contraseña</string>
+ <string name="missing_presence_updates">Suscripción de actualizaciones de presencia del contacto perdida</string>
+ <string name="request_presence_updates">Por favor, solicita la suscripción de presencia a tu contacto primero.\n\n<small>Esto será usado para determinar qué cliente(s) está usando tu contacto.</small></string>
+ <string name="request_now">Solicitar ahora</string>
+ <string name="delete_fingerprint">Eliminar Clave OTR</string>
+ <string name="sure_delete_fingerprint">¿Estás seguro que quieres eliminar la clave OTR?</string>
+ <string name="ignore">Ignorar</string>
+ <string name="without_mutual_presence_updates"><b>Aviso:</b> Enviando esto sin suscripción de presencia por ambas partes podría causar problemas inesperados.\n\n<small>Verficia la suscripción de presencia en detalles del contacto.</small></string>
+ <string name="pref_encryption_settings">Ajustes de encriptación</string>
+ <string name="pref_force_encryption">Forzar encriptación end-to-end</string>
+ <string name="pref_force_encryption_summary">Siempre enviar mensajes encriptados (excepto para conferencias)</string>
+ <string name="pref_dont_save_encrypted">No guardar mensajes encriptados</string>
+ <string name="pref_dont_save_encrypted_summary">Aviso: Esto podría llevar a pérdida de mensajes</string>
+ <string name="pref_expert_options">Ajustes avanzados</string>
+ <string name="pref_expert_options_summary">Por favor, cuidado con estas opciones</string>
+ <string name="pref_use_larger_font">Incrementar tamaño de fuente</string>
+ <string name="pref_use_larger_font_summary">Usar fuentes grandes en toda la aplicación</string>
+ <string name="pref_use_send_button_to_indicate_status">Botón enviar indica estado</string>
+ <string name="pref_use_indicate_received">Solicitar entrega de mensaje</string>
+ <string name="pref_use_indicate_received_summary">Cuando el contacto reciba el mensaje será indicado con una marca verde. Cuidado, esto podría no funcionar en todos los casos.</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">El color del botón enviar indica el estado del contacto</string>
+ <string name="pref_expert_options_other">Otros</string>
+ <string name="pref_conference_name">Nombre de conferencia</string>
+ <string name="pref_conference_name_summary">Usar el asunto de la conferencia en lugar del identificador jabber como nombre de conferencia</string>
+ <string name="toast_message_otr_fingerprint">¡Clave OTR copiada en el portapapeles!</string>
+ <string name="conference_banned">Tu entrada a esta conferencia ha sido prohibida</string>
+ <string name="conference_members_only">Esta conferencia es solo para miembros</string>
+ <string name="conference_kicked">Has sido expulsado de esta conferencia</string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-eu/arrays.xml b/src/main/res/values-eu/arrays.xml
new file mode 100644
index 00000000..a34d3c6a
--- /dev/null
+++ b/src/main/res/values-eu/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mugikorra</item>
+ <item>Telefonoa</item>
+ <item>Tableta</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>inoiz</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 minutu</item>
+ <item>ordu bat</item>
+ <item>2 ordu</item>
+ <item>8 ordu</item>
+ <item>abisatu arte</item>
+ </string-array>
+
+ <integer-array name="mute_options_durations">
+ <item>1800</item>
+ <item>3600</item>
+ <item>7200</item>
+ <item>28800</item>
+ <item>-1</item>
+ </integer-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml
new file mode 100644
index 00000000..43c141ea
--- /dev/null
+++ b/src/main/res/values-eu/strings.xml
@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Ezarpenak</string>
+ <string name="action_add">Elkarrizketa berria</string>
+ <string name="action_accounts">Kontuak kudeatu</string>
+ <string name="action_end_conversation">Elkarrizketa hau amaitu</string>
+ <string name="action_contact_details">Kontaktuaren xehetasunak</string>
+ <string name="action_muc_details">Konferentziaren xehetasunak</string>
+ <string name="action_secure">Elkarrizketa segurua</string>
+ <string name="action_add_account">Kontua gehitu</string>
+ <string name="action_edit_contact">Izena editatu</string>
+ <string name="action_add_phone_book">Telefono kontaktuetara gehitu</string>
+ <string name="action_delete_contact">Zerrendatik ezabatu</string>
+ <string name="title_activity_manage_accounts">Kontuak kudeatu</string>
+ <string name="title_activity_settings">Ezarpenak</string>
+ <string name="title_activity_conference_details">Konferentziaren xehetasunak</string>
+ <string name="title_activity_contact_details">Kontaktuaren xehetasunak</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">Elkarrizketa batekin partekatu</string>
+ <string name="title_activity_start_conversation">Elkarrizketa hasi</string>
+ <string name="title_activity_choose_contact">Kontaktua hautatu</string>
+ <string name="just_now">orain</string>
+ <string name="minute_ago">min 1 lehenago</string>
+ <string name="minutes_ago">%d min lehenago</string>
+ <string name="unread_conversations">irakurri gabeko elkarrizketak</string>
+ <string name="sending">bidaltzen&#8230;</string>
+ <string name="encrypted_message">Mezua desenkriptatzen. Mesedez itxaron&#8230;</string>
+ <string name="nick_in_use">Ezizena erabilita dagoeneko</string>
+ <string name="admin">Administratzailea</string>
+ <string name="owner">Jabea</string>
+ <string name="moderator">Moderatzailea</string>
+ <string name="participant">Parte-hartzailea</string>
+ <string name="visitor">Bisitaria</string>
+ <string name="remove_contact_text">%s zure zerrendatik ezabatu nahi duzu? Kontu honekin lotutako elkarrizketa ez da ezabatuko.</string>
+ <string name="remove_bookmark_text">%s laster-marka bezala ezabatu nahi duzu? Laster-marka honekin lotutako elkarrizketa ez da ezabatuko.</string>
+ <string name="register_account">Kontu berria zerbitzarian erregistratu</string>
+ <string name="share_with">Honekin partekatu</string>
+ <string name="start_conversation">Elkarrizketa hasi</string>
+ <string name="invite_contact">Kontaktu bat gonbidatu</string>
+ <string name="contacts">Kontaktuak</string>
+ <string name="cancel">Utzi</string>
+ <string name="add">Gehitu</string>
+ <string name="edit">Editatu</string>
+ <string name="delete">Ezabatu</string>
+ <string name="save">Gorde</string>
+ <string name="ok">Ados</string>
+ <string name="crash_report_title">Conversations gelditu da</string>
+ <string name="crash_report_message">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu\n<b>Abisua:</b> Honek zure XMPP kontua erabiliko du garatzaileari akats harraska bidaltzeko.</string>
+ <string name="send_now">Bidali orain</string>
+ <string name="send_never">Ez galdetu berriz</string>
+ <string name="problem_connecting_to_account">Ezin izan da kontura konektatu</string>
+ <string name="problem_connecting_to_accounts">Ezin izan da hainbat kontuetara konektatu</string>
+ <string name="touch_to_fix">Ukitu hemen zure kontuak kudeatzeko</string>
+ <string name="attach_file">Fitxategia erantsi</string>
+ <string name="not_in_roster">Kontaktua ez dago zure zerrendan. Gehitu nahiko al zenuke?</string>
+ <string name="add_contact">Kontaktua gehitu</string>
+ <string name="send_failed">huts bidaltzerakoan</string>
+ <string name="send_rejected">ukatua</string>
+ <string name="receiving_image">Irudi fitxategia jasotzen. Mesedez itxaron&#8230;</string>
+ <string name="preparing_image">Irudia transmisiorako prestatzen. Mesedez itxaron&#8230;</string>
+ <string name="action_clear_history">Historia garbitu</string>
+ <string name="clear_conversation_history">Elkarrizketa historia garbitu</string>
+ <string name="clear_histor_msg">Elkarrizketa honetako mezu guztiak ezabatu nahi al dituzu?\n\n<b>Abisua:</b> Honek ez du beste gailu edo zerbitzarietan gordetako mezuetan eraginik izango.</string>
+ <string name="delete_messages">Mezuak ezabatu</string>
+ <string name="also_end_conversation">Elkarrizketa hau jarraian amaitu</string>
+ <string name="choose_presence">Hautatu agerpena kontaktuarentzat</string>
+ <string name="send_plain_text_message">Testu mezua bidali</string>
+ <string name="send_otr_message">OTRz enkriptatutako mezua bidali</string>
+ <string name="send_pgp_message">OpenPGPz enkriptatutako mezua bidali</string>
+ <string name="your_nick_has_been_changed">Zure ezizena aldatu da</string>
+ <string name="download_image">Irudia deskargatu</string>
+ <string name="image_offered_for_download"><i>Irudi fitxategia deskargarako eskeinia</i></string>
+ <string name="send_unencrypted">Enkriptatu gabe bidali</string>
+ <string name="decryption_failed">Desenkriptazioak huts egin du. Agian ez duzu gako pribatu egokia.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversationsek <b>OpenKeychain</b> izeneko hirugarren app bat erabiltzen du mezuak enkriptatu eta desenkriptatzeko eta zure gako publikoak kudeatzeko.\n\nOpenKeychain GPLv3 lizentziapean dago eta F-Droid eta Google Playn eskura daiteke.\n\n<small>(Mesedez ondoren Conversations berrabiarazi)</small></string>
+ <string name="restart">Berrabiarazi</string>
+ <string name="install">Instalatu</string>
+ <string name="offering">eskeintzen&#8230;</string>
+ <string name="waiting">itxaroten&#8230;</string>
+ <string name="no_pgp_key">Ez da OpenPGP gakorik aurkitu</string>
+ <string name="contact_has_no_pgp_key">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktua bere gako publikoa jakinarazten ez dagoelako.\n\n<small>Mesedez eskatu ezaiozu zure kontaktuari openPGP konfigura dezan.</small></string>
+ <string name="no_pgp_keys">Ez da OpenPGP gakorik aurkitu</string>
+ <string name="contacts_have_no_pgp_keys">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktuak haien gako publikoa jakinarazten ez daudelako.\n\n<small>Mesedez eskatu ezaiezu zure kontakuei OpenPGP konfigura dezaten.</small></string>
+ <string name="encrypted_message_received"><i>Enkriptatutako mezua jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string>
+ <string name="encrypted_image_received"><i>Enkriptatutako irudia jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string>
+ <string name="image_file"><i>Irudia jaso da. Ukitu ikusteko</i></string>
+ <string name="pref_general">Orokorrak</string>
+ <string name="pref_xmpp_resource">XMPP baliabidea</string>
+ <string name="pref_xmpp_resource_summary">Bezero honek bere burua aurkezteko erabiltzen duen izena</string>
+ <string name="pref_accept_files">Fitxategiak onartu</string>
+ <string name="pref_accept_files_summary">Hurrengo tamaina baino fitxategi txikiagoak automatikoki onartu&#8230;</string>
+ <string name="pref_notification_settings">Jakinarazpenen ezarpenak</string>
+ <string name="pref_notifications">Jakinarazpenak</string>
+ <string name="pref_notifications_summary">Mezu berri bat heltzerakoan jakinarazi</string>
+ <string name="pref_vibrate">Dardaratu</string>
+ <string name="pref_vibrate_summary">Dardaratu ere mezu berri bat heltzerakoan</string>
+ <string name="pref_sound">Soinua</string>
+ <string name="pref_sound_summary">Dei-tonua jo jakinarazpenarekin</string>
+ <string name="pref_conference_notifications">Konferentzien jakinarazpenak</string>
+ <string name="pref_conference_notifications_summary">Beti jakinarazi konferentzia mezu berri bat heltzerakoan eta ez soilik nabarmentzerakoan</string>
+ <string name="pref_notification_grace_period">Jakinarazpenen grazia epea</string>
+ <string name="pref_notification_grace_period_summary">Jakinarazpenak denbora labur baterako ezgaitu ikatz-kopia bat jaso ondoren</string>
+ <string name="pref_advanced_options">Aukera aurreratuak</string>
+ <string name="pref_never_send_crash">Gelditze txostenik ez bidali inoiz</string>
+ <string name="pref_never_send_crash_summary">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu</string>
+ <string name="pref_confirm_messages">Mezuak egiaztatu</string>
+ <string name="pref_confirm_messages_summary">Zure kontaktuak mezu bat noiz jaso eta irakurri duzun jakin dezan baimendu</string>
+ <string name="pref_ui_options">Erabiltzaile-interfazearen aukerak</string>
+ <string name="openpgp_error">OpenKeychainek akats baten berri eman du</string>
+ <string name="error_decrypting_file">Sarrera/Irteera akatsa fitxategia desenkriptatzerakoan</string>
+ <string name="accept">Onartu</string>
+ <string name="error">Akats bat gertatu da</string>
+ <string name="pref_grant_presence_updates">Presentzia eguneraketak eman</string>
+ <string name="pref_grant_presence_updates_summary">Prebentiboki presentzia eguneraketak eman eta eskatu sortu dituzun kontaktuetarako</string>
+ <string name="subscriptions">Harpidetzak</string>
+ <string name="your_account">Zure kontua</string>
+ <string name="keys">Gakoak</string>
+ <string name="send_presence_updates">Presentzia eguneraketak bidali</string>
+ <string name="receive_presence_updates">Presentzia eguneraketak jaso</string>
+ <string name="ask_for_presence_updates">Presentzia eguneraketak eskatu</string>
+ <string name="attach_choose_picture">Argazkia aukeratu</string>
+ <string name="attach_take_picture">Argazkia egin</string>
+ <string name="preemptively_grant">Prebentiboki harpidetza eskaera eman</string>
+ <string name="error_not_an_image_file">Aukeratu duzun fitxategia ez da irudi bat</string>
+ <string name="error_compressing_image">Huts irudi fitxategia bihurtzerakoan</string>
+ <string name="error_file_not_found">Fitxategia ez da aurkitu</string>
+ <string name="error_io_exception">Sarrera/Irteera akats orokorra. Agian biltegian lekurik gabe gelditu zara?</string>
+ <string name="error_security_exception_during_image_copy">Irudi hau aukeratzeko erabili duzun aplikazioak ez digu fitxategia irakurtzeko baimen nahikorik eman.\n\n<small>Beste fitxategi kudeatzaile bat erabili ezazu irudia aukeratzeko</small></string>
+ <string name="account_status_unknown">Ezezaguna</string>
+ <string name="account_status_disabled">Aldi baterako ezgaituta</string>
+ <string name="account_status_online">Konektatuta</string>
+ <string name="account_status_connecting">Konektatzen\u2026</string>
+ <string name="account_status_offline">Lineaz kanpo</string>
+ <string name="account_status_unauthorized">Ez baimenduta</string>
+ <string name="account_status_not_found">Zerbitzaria ez da aurkitu</string>
+ <string name="account_status_no_internet">Konektagarritasunik ez</string>
+ <string name="account_status_regis_fail">Erregistroak huts egin du</string>
+ <string name="account_status_regis_conflict">Erabiltzaile izena dagoeneko erabilita</string>
+ <string name="account_status_regis_success">Erregistroa burutu da</string>
+ <string name="account_status_regis_not_sup">Zerbitzariak ez du erregistratzea onartzen</string>
+ <string name="encryption_choice_none">Testu laua</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Kontua editatu</string>
+ <string name="mgmt_account_delete">Kontua ezabatu</string>
+ <string name="mgmt_account_disable">Aldi baterako ezgaitu</string>
+ <string name="mgmt_account_publish_avatar">Profileko argazkia argitaratu</string>
+ <string name="mgmt_account_publish_pgp">OpenPGP gako publikoa argitaratu</string>
+ <string name="mgmt_account_enable">Kontua gaitu</string>
+ <string name="mgmt_account_are_you_sure">Ziur al zaude?</string>
+ <string name="mgmt_account_delete_confirm_text">Zure kontua ezabatzen baduzu zure elkarrizketa historia guztia galduko da</string>
+ <string name="attach_record_voice">Ahotsa grabatu</string>
+ <string name="account_settings_jabber_id">Jabber IDa</string>
+ <string name="account_settings_password">Pasahitza</string>
+ <string name="account_settings_example_jabber_id">erabiltzailea@adibidea.com</string>
+ <string name="account_settings_confirm_password">Pasahitza egiaztatu</string>
+ <string name="password">Pasahitza</string>
+ <string name="confirm_password">Pasahitza egiaztatu</string>
+ <string name="passwords_do_not_match">Pasahitzak ez dute bat egiten</string>
+ <string name="invalid_jid">Hau ez da Jabber ID baliodun bat</string>
+ <string name="error_out_of_memory">Memoriarik gabe. Irudia handiegia da</string>
+ <string name="add_phone_book_text">%s zure telefono kontaktu zerrendara gehitu nahi al duzu?</string>
+ <string name="contact_status_online">konektatuta</string>
+ <string name="contact_status_free_to_chat">hitzegiteko aske</string>
+ <string name="contact_status_away">kanpoan</string>
+ <string name="contact_status_extended_away">luzerako kanpoan</string>
+ <string name="contact_status_do_not_disturb">ez gogaitu</string>
+ <string name="contact_status_offline">lineaz kanpo</string>
+ <string name="muc_details_conference">Konferentzia</string>
+ <string name="muc_details_other_members">Beste kideak</string>
+ <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+ <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+ <string name="server_info_pep">XEP-0163: PEP (Profileko argazkiak)</string>
+ <string name="server_info_available">eskuragarri</string>
+ <string name="server_info_unavailable">ez eskuragarri</string>
+ <string name="missing_public_keys">Gako publikoen iragarpenak faltan</string>
+ <string name="last_seen_now">azkenengoz ikusia orain</string>
+ <string name="last_seen_mins">azkenengoz ikusia %d minutu lehenago</string>
+ <string name="last_seen_hours">azkenengoz ikusia %d ordu lehenago</string>
+ <string name="last_seen_days">azkenengoz ikusia %d egun lehenago</string>
+ <string name="never_seen">inoiz ez ikusia</string>
+ <string name="last_seen_min">azkenengoz ikusia minutu 1 lehenago</string>
+ <string name="last_seen_hour">azkenengoz ikusia ordu 1 lehenago</string>
+ <string name="last_seen_day">azkenengoz ikusia egun 1 lehenago</string>
+ <string name="install_openkeychain">Mezu enkriptatua. Mesedez instalatu OpenKeychain desenkriptatzeko.</string>
+ <string name="unknown_otr_fingerprint">OTR hatz-marka ezezaguna</string>
+ <string name="openpgp_messages_found">OpenPGPz enkriptatutako mezuak aurkitu dira</string>
+ <string name="reception_failed">Jasotzeak huts egin du</string>
+ <string name="your_fingerprint">Zure hatz-marka</string>
+ <string name="otr_fingerprint">OTR hatz-marka</string>
+ <string name="verify">Egiaztatu</string>
+ <string name="decrypt">Desenkriptatu</string>
+ <string name="conferences">Konferentziak</string>
+ <string name="search">Bilatu</string>
+ <string name="create_contact">Kontaktua sortu</string>
+ <string name="join_conference">Konferentziara batu</string>
+ <string name="delete_contact">Kontaktua ezabatu</string>
+ <string name="view_contact_details">Kontaktuaren xehetasunak ikusi</string>
+ <string name="create">Sortu</string>
+ <string name="contact_already_exists">Kontaktua existitzen da dagoeneko</string>
+ <string name="join">Batu</string>
+ <string name="conference_address">Konferentziaren helbidea</string>
+ <string name="conference_address_example">gela@conference.example.com</string>
+ <string name="save_as_bookmark">Gorde laster-marka bezala</string>
+ <string name="delete_bookmark">Laster-marka ezabatu</string>
+ <string name="bookmark_already_exists">Laster-marka hau existitzen da dagoeneko</string>
+ <string name="you">Zu</string>
+ <string name="action_edit_subject">Konferentziaren gaia editatu</string>
+ <string name="conference_not_found">Konferentzia ez da aurkitu</string>
+ <string name="leave">Alde egin</string>
+ <string name="contact_added_you">Kontaktuak bere zerrendara gehitu zaitu</string>
+ <string name="add_back">Bera gehitu</string>
+ <string name="contact_has_read_up_to_this_point">%s(e)k puntu honetaraino irakurri du</string>
+ <string name="publish">Argitaratu</string>
+ <string name="touch_to_choose_picture">Ukitu profileko argazkia irudi bat galeriatik hautatzeko</string>
+ <string name="publish_avatar_explanation">Adi: Zure presentzia eguneraketetara harpidetutako edonork irudi hau ikusi ahal izango du.</string>
+ <string name="publishing">Argitaratzen&#8230;</string>
+ <string name="error_publish_avatar_server_reject">Zerbitzariak zure argitarapena ukatu du</string>
+ <string name="error_publish_avatar_converting">Zerbait oker joan da zure irudia bihurtzerakoan</string>
+ <string name="error_saving_avatar">Ezin izan da profileko argazkia diskoan gorde</string>
+ <string name="or_long_press_for_default">(Edo sakatu luze lehenetsira bueltatzeko)</string>
+ <string name="error_publish_avatar_no_server_support">Zure zerbitzariak ez du profileko argazkien argitarapena onartzen</string>
+ <string name="private_message">xuxurlatu</string>
+ <string name="private_message_to">%s(r)i</string>
+ <string name="send_private_message_to">%s(r)i mezu pribatua bidali</string>
+ <string name="connect">Konektatu</string>
+ <string name="account_already_exists">Kontu hau existitzen da dagoeneko</string>
+ <string name="next">Hurrengoa</string>
+ <string name="server_info_session_established">Uneko saioa ezarri da</string>
+ <string name="additional_information">Informazio gehiago</string>
+ <string name="skip">Orain ez</string>
+ <string name="disable_notifications">Jakinarazpenak ezgaitu</string>
+ <string name="disable_notifications_for_this_conversation">Elkarrizketa honetarako jakinarazpenak ezgaitu</string>
+ <string name="notifications_disabled">Jakinarazpenak ezgaituta daude</string>
+ <string name="enable">Gaitu</string>
+ <string name="conference_requires_password">Konferentziak pasahitza behar du</string>
+ <string name="enter_password">Sartu pasahitza</string>
+ <string name="missing_presence_updates">Kontaktuaren presentzia eguneraketak falta dira</string>
+ <string name="request_presence_updates">Mesedez eskatu lehenago zure kontaktuaren presentzia eguneraketak.\n\n<small>Kontaktuak erabiltzen ari den bezeroa(k) zehazteko erabilika da hau.</small></string>
+ <string name="request_now">Eskatu orain</string>
+ <string name="delete_fingerprint">Hatz-marka ezabatu</string>
+ <string name="sure_delete_fingerprint">Ziur al zaude hatz-marka hau ezabatu nahi duzulaz?</string>
+ <string name="ignore">Kasurik ez egin</string>
+ <string name="without_mutual_presence_updates"><b>Adi:</b> Bien arteko presentzia eguneraketarik gabe hau bidaltzeak ustekabeko arazoak sor litzake.\n\n<small>Joan zaitez kontaktuaren xehetasunetara zure presentzia eguneraketak egiaztatzeko.</small></string>
+ <string name="pref_encryption_settings">Enkriptazio ezarpenak</string>
+ <string name="pref_force_encryption">End-to-end enkriptazioa behartu</string>
+ <string name="pref_force_encryption_summary">Mezuak beti enkriptatuta bidali (konferentzietan izan ezik)</string>
+ <string name="pref_dont_save_encrypted">Ez gorde enkriptatutako mezuak</string>
+ <string name="pref_dont_save_encrypted_summary">Adi: Honek mezuen galera ekar lezake</string>
+ <string name="pref_enable_legacy_ssl">Oinordetutako SSL gaitu</string>
+ <string name="pref_enable_legacy_ssl_summary">SSLv3 gaitzen du oinordetutako zerbitzarietarako. Adi: SSLv3 ez segurutzat hartzen da.</string>
+ <string name="pref_expert_options">Adituentzako aukerak</string>
+ <string name="pref_expert_options_summary">Mesedez kontuz ibili hauekin</string>
+ <string name="pref_use_larger_font">Letraren tamaina handitu</string>
+ <string name="pref_use_larger_font_summary">Letra tamaina handiagoa erabili aplikazio osoan zehar</string>
+ <string name="pref_use_send_button_to_indicate_status">Bidaltze botoiak egoera adierazten du</string>
+ <string name="pref_use_indicate_received">Mezuen jasotzea eskatu</string>
+ <string name="pref_use_indicate_received_summary">Jasotako mezuak marka berde batekin markatuko dira. Baliteke kasu guztietan ez funtzionatzea.</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Bidaltze botoia koloreztatu kontaktu baten egoera adierazteko</string>
+ <string name="pref_expert_options_other">Besteak</string>
+ <string name="pref_conference_name">Konferentziaren izena</string>
+ <string name="pref_conference_name_summary">Erabili gelaren gaia konferentziak identifikatzeko eta ez JIDa</string>
+ <string name="toast_message_otr_fingerprint">OTR hatz-marka arbelara kopiatu da</string>
+ <string name="conference_banned">Konferentzia honetara sartzea debekatuta duzu</string>
+ <string name="conference_members_only">Konferentzia hau kideentzat da soilik</string>
+ <string name="conference_kicked">Konferentzia honetatik kanporatua izan zara</string>
+ <string name="using_account">%s kontua erabiltzen</string>
+ <string name="checking_image">Irudia egiaztatzen HTTP ostalarian</string>
+ <string name="image_file_deleted">Irudia ezabatu egin da</string>
+ <string name="not_connected_try_again">Ez zaude konektatuta. Saiatu beranduago berriz</string>
+ <string name="check_image_filesize">Irudiaren tamaina egiaztatu</string>
+
+</resources>
diff --git a/src/main/res/values-fr/arrays.xml b/src/main/res/values-fr/arrays.xml
new file mode 100644
index 00000000..ae140796
--- /dev/null
+++ b/src/main/res/values-fr/arrays.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mobile</item>
+ <item>Téléphone</item>
+ <item>Tablette</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>jamais</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..e1db316d
--- /dev/null
+++ b/src/main/res/values-fr/strings.xml
@@ -0,0 +1,273 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Paramètres</string>
+ <string name="action_add">Nouvelle conversation</string>
+ <string name="action_accounts">Gérer les comptes</string>
+ <string name="action_end_conversation">Terminer cette conversation</string>
+ <string name="action_contact_details">Détails du contact</string>
+ <string name="action_muc_details">Détails de la conférence</string>
+ <string name="action_secure">Conversation sécurisée</string>
+ <string name="action_add_account">Ajouter un compte</string>
+ <string name="action_edit_contact">Modifier le nom</string>
+ <string name="action_add_phone_book">Ajouter aux contacts</string>
+ <string name="action_delete_contact">Retirer des contacts</string>
+ <string name="title_activity_manage_accounts">Gestion des comptes</string>
+ <string name="title_activity_settings">Paramètres</string>
+ <string name="title_activity_conference_details">Détails de la conférence</string>
+ <string name="title_activity_contact_details">Détails du contact</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">Partager avec Conversation</string>
+ <string name="title_activity_start_conversation">Lancement de Conversation</string>
+ <string name="title_activity_choose_contact">Choix du contact</string>
+ <string name="just_now">À l\'instant</string>
+ <string name="minute_ago">Il y a 1 minute</string>
+ <string name="minutes_ago">Il y a %d minutes</string>
+ <string name="unread_conversations">Conversations non lues</string>
+ <string name="sending">envoi&#8230;</string>
+ <string name="encrypted_message">Déchiffrement du message. Patientez&#8230;</string>
+ <string name="nick_in_use">Cet identifiant est déjà utilisé.</string>
+ <string name="admin">Administrateur</string>
+ <string name="owner">Propriétaire</string>
+ <string name="moderator">Modérateur</string>
+ <string name="participant">Participant</string>
+ <string name="visitor">Visiteur</string>
+ <string name="remove_contact_text">Voulez-vous supprimer %s de votre liste? Les conversations associées à ce compte ne seront pas supprimées.</string>
+ <string name="remove_bookmark_text">Voulez-vous retirer %s des favoris? La conversation associée avec ce favoris ne sera pas supprimé.</string>
+ <string name="register_account">Créer un nouveau compte sur le serveur</string>
+ <string name="share_with">Partager avec</string>
+ <string name="start_conversation">Démarrer une conversation</string>
+ <string name="invite_contact">Inviter des contacts</string>
+ <string name="contacts">Contacts</string>
+ <string name="cancel">Annuler</string>
+ <string name="add">Ajouter</string>
+ <string name="edit">Modifier</string>
+ <string name="delete">Supprimer</string>
+ <string name="save">Enregistrer</string>
+ <string name="ok">OK</string>
+ <string name="crash_report_title">Conversations s\'est arreté</string>
+ <string name="crash_report_message">En envoyant des logs vous aidez au développement de Conversations.\n\n<b>Attention:</b> Votre compte XMPP sera utilisé pour envoyer les logs aux développeurs.</string>
+ <string name="send_now">Envoyer</string>
+ <string name="send_never">Ne plus me demander</string>
+ <string name="problem_connecting_to_account">Impossible de se connecter au compte.</string>
+ <string name="problem_connecting_to_accounts">Impossible de se connecter aux comptes.</string>
+ <string name="touch_to_fix">Appuyez pour gérer vos comptes.</string>
+ <string name="attach_file">Lier un fichier</string>
+ <string name="not_in_roster">Le contact n\'est pas dans votre carnet d\'adresses. Voulez-vous l\'y ajouter?</string>
+ <string name="add_contact">Ajouter un contact</string>
+ <string name="send_failed">Echec de l\'envoi.</string>
+ <string name="send_rejected">Rejeté</string>
+ <string name="receiving_image">Réception d\'une image. Patientez&#8230;</string>
+ <string name="preparing_image">Préparation de la transmission de l\'image. Patientez&#8230;</string>
+ <string name="action_clear_history">Vider l\'historique</string>
+ <string name="clear_conversation_history">Vider l\'historique de la conversation</string>
+ <string name="clear_histor_msg">Voulez-vous supprimer tous les messages de cette conversation?\n\n<b>Attention:</b> Les messages seront supprimés uniquement sur ce périphérique.</string>
+ <string name="delete_messages">Supprimer les messages</string>
+ <string name="also_end_conversation">Terminer plus tard cette conversation</string>
+ <string name="choose_presence">Choisir le status de présence</string>
+ <string name="send_plain_text_message">Envoyer un message</string>
+ <string name="send_otr_message">Envoyer un message sécurisé par OTR</string>
+ <string name="send_pgp_message">Envoyer un message sécurisé par OpenPGP</string>
+ <string name="your_nick_has_been_changed">Votre identifiant a été changé</string>
+ <string name="download_image">Télécharger l\'image</string>
+ <string name="image_offered_for_download"><i>Image proposée au téléchargement.</i></string>
+ <string name="send_unencrypted">Envoyer en clair</string>
+ <string name="decryption_failed">Echec du déchiffrement. Merci de vérifier la clef privée utilisée.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations requiert une application tierce nommée <b>OpenKeychain</b> pour chiffrer et déchiffrer les messages.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n<small>(Merci de redémarrer Conversations apres l\'installation du logiciel)</small></string>
+ <string name="restart">Redémarrer</string>
+ <string name="install">Installer</string>
+ <string name="offering">Proposition&#8230;</string>
+ <string name="waiting">Patientez&#8230;</string>
+ <string name="no_pgp_key">Aucune clef OpenPGP trouvée.</string>
+ <string name="contact_has_no_pgp_key">Conversations ne peut chiffrer vos messages car votre correspondant n\'a pas communiqué sa clef publique.\n\n<small>Merci de demander à votre correspondant de configurer OpenPGP.</small></string>
+ <string name="no_pgp_keys">Aucune clef OpenPGP n\'est disponible.</string>
+ <string name="contacts_have_no_pgp_keys">Conversations ne peut pas chiffrer votre message car vous ne connaissez pas la clef publique de vos contacts.\n\n<small>Merci de les faire configurer leur OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Message chiffré reçu. Appuyez pour le déchiffrer.</i></string>
+ <string name="encrypted_image_received"><i>Image chiffrée reçue. Appuyez pour la déchiffrer.</i></string>
+ <string name="image_file"><i>Image reçue. Appuyez pour visualiser.</i></string>
+ <string name="pref_general">Général</string>
+ <string name="pref_xmpp_resource">Ressource XMPP</string>
+ <string name="pref_xmpp_resource_summary">Nom permettant d\'identifier ce client XMPP</string>
+ <string name="pref_accept_files">Accepter les fichiers</string>
+ <string name="pref_accept_files_summary">Accepter automatiquement les fichiers plus petits que&#8230;</string>
+ <string name="pref_notification_settings">Paramètres de notification</string>
+ <string name="pref_notifications">Notifications</string>
+ <string name="pref_notifications_summary">Notifier l\'arrivée d\'un message</string>
+ <string name="pref_vibrate">Vibration</string>
+ <string name="pref_vibrate_summary">Vibrer lors de l\'arrivée d\'un message</string>
+ <string name="pref_sound">Sonore</string>
+ <string name="pref_sound_summary">Jouer une sonnerie lors de l\'arrivée d\'un message</string>
+ <string name="pref_conference_notifications">Notifications lors des conférences</string>
+ <string name="pref_conference_notifications_summary">Toujours notifier l\'arrivée d\'un message provenant d\'une conférence.</string>
+ <string name="pref_notification_grace_period">Période sans notification</string>
+ <string name="pref_notification_grace_period_summary">Désactiver momentanément les notifications après l\'arrivée d\'une copie carbone.</string>
+ <string name="pref_advanced_options">Options avancées</string>
+ <string name="pref_never_send_crash">Ne jamais envoyer de rapports d\'erreurs</string>
+ <string name="pref_never_send_crash_summary">En envoyant des logs vous aidez au développement de Conversations.</string>
+ <string name="pref_confirm_messages">Confirmation de lecture</string>
+ <string name="pref_confirm_messages_summary">Informer l\'expéditeur d\'un message de sa bonne réception.</string>
+ <string name="pref_ui_options">Options d\'affichage</string>
+ <string name="openpgp_error">Une erreur s\'est produite via OpenKeychain</string>
+ <string name="error_decrypting_file">Erreur d\'E/S lors du déchiffrement du fichier</string>
+ <string name="accept">Accepter</string>
+ <string name="error">Une erreur s\'est produite</string>
+ <string name="pref_grant_presence_updates">Accepter les mises à jour de présence</string>
+ <string name="pref_grant_presence_updates_summary">Demander et accepter par avance les mises à jour de présence des contacts créés.</string>
+ <string name="subscriptions">Publications</string>
+ <string name="your_account">Votre compte</string>
+ <string name="keys">Clefs</string>
+ <string name="send_presence_updates">Envoyer les mises à jour de présence</string>
+ <string name="receive_presence_updates">Recevoir les mises à jour de présence</string>
+ <string name="ask_for_presence_updates">Demander les mises à jour de présence</string>
+ <string name="attach_choose_picture">Choisir une image</string>
+ <string name="attach_take_picture">Prendre une photo</string>
+ <string name="preemptively_grant">Accepter par avance les demandes de publication.</string>
+ <string name="error_not_an_image_file">Le fichier choisi n\'est pas une image</string>
+ <string name="error_compressing_image">Une erreur s\'est produite en convertissant l\'image</string>
+ <string name="error_file_not_found">Fichier non trouvé</string>
+ <string name="error_io_exception">Erreur générale d\'E/S. Avez-vous encore de l\'espace libre?</string>
+ <string name="error_security_exception_during_image_copy">L\'application utilisée empêche la lecture de l\'image.\n\n<small>Choisissez l\'image depuis une autre application.</small></string>
+ <string name="account_status_unknown">Inconnu</string>
+ <string name="account_status_disabled">Désactivé temporairement</string>
+ <string name="account_status_online">En ligne</string>
+ <string name="account_status_connecting">Connexion\u2026</string>
+ <string name="account_status_offline">Hors-ligne</string>
+ <string name="account_status_unauthorized">Non autorisé</string>
+ <string name="account_status_not_found">Serveur non trouvé</string>
+ <string name="account_status_no_internet">Aucune connectivité</string>
+ <string name="account_status_regis_fail">Enregistrement échoué</string>
+ <string name="account_status_regis_conflict">Identifiant déjà utilisé</string>
+ <string name="account_status_regis_success">Enregistrement réussi</string>
+ <string name="account_status_regis_not_sup">Le serveur ne permet pas l\'enregistrement</string>
+ <string name="encryption_choice_none">Texte clair</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Modifier le compte</string>
+ <string name="mgmt_account_delete">Supprimer</string>
+ <string name="mgmt_account_disable">Désactiver temporairement</string>
+ <string name="mgmt_account_publish_avatar">Publier un avatar</string>
+ <string name="mgmt_account_publish_pgp">Publier la clef publique OpenPGP</string>
+ <string name="mgmt_account_enable">Activer</string>
+ <string name="mgmt_account_are_you_sure">Êtes-vous sûr?</string>
+ <string name="mgmt_account_delete_confirm_text">En supprimant votre compte, votre historique de conversations sera perdu!</string>
+ <string name="attach_record_voice">Enregistrer un son</string>
+ <string name="account_settings_jabber_id">Identifiant</string>
+ <string name="account_settings_password">Mot de passe</string>
+ <string name="account_settings_example_jabber_id">utilisateur@exemple.com</string>
+ <string name="account_settings_confirm_password">Confirmer le mot de passe</string>
+ <string name="password">Mot de passe</string>
+ <string name="confirm_password">Confirmer le mot de passe</string>
+ <string name="passwords_do_not_match">Les deux mots de passes ne correspondent pas.</string>
+ <string name="invalid_jid">Ce n\'est pas un identifiant valide.</string>
+ <string name="error_out_of_memory">Plus de mémoire disponible. L\'image est trop volumineuse.</string>
+ <string name="add_phone_book_text">Voulez-vous ajouter %s aux contacts du téléphone?</string>
+ <string name="contact_status_online">En ligne</string>
+ <string name="contact_status_free_to_chat">Disponible</string>
+ <string name="contact_status_away">Absent</string>
+ <string name="contact_status_extended_away">Absent depuis longtemps</string>
+ <string name="contact_status_do_not_disturb">Ne pas déranger</string>
+ <string name="contact_status_offline">Hors-ligne</string>
+ <string name="muc_details_conference">Conférence</string>
+ <string name="muc_details_other_members">Autres membres</string>
+ <string name="server_info_carbon_messages">Copies carbone</string>
+ <string name="server_info_stream_management">Gestion des flux</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+ <string name="server_info_available">disponible</string>
+ <string name="server_info_unavailable">indisponible</string>
+ <string name="missing_public_keys">Aucune annonce de clef publique</string>
+ <string name="last_seen_now">en ligne à l\'instant</string>
+ <string name="last_seen_min">en ligne il y a 1 minute</string>
+ <string name="last_seen_mins">en ligne il y a %d minutes</string>
+ <string name="last_seen_hour">en ligne il y a 1 heure</string>
+ <string name="last_seen_hours">en ligne il y a %d heures</string>
+ <string name="last_seen_day">en ligne hier</string>
+ <string name="last_seen_days">en ligne il y a %d jours</string>
+ <string name="never_seen">jamais vu en ligne</string>
+ <string name="install_openkeychain">Message chiffré. Merci d\'installer OpenKeychain pour lire le contenu du message.</string>
+ <string name="unknown_otr_fingerprint">Empreinte OTR inconnue.</string>
+ <string name="openpgp_messages_found">Messages chiffrés par OpenPGP détectés.</string>
+ <string name="reception_failed">Echec lors de la réception</string>
+ <string name="your_fingerprint">Votre empreinte</string>
+ <string name="otr_fingerprint">Empreinte OTR</string>
+ <string name="verify">Vérifier</string>
+ <string name="decrypt">Déchiffrer</string>
+ <string name="conferences">Conférences</string>
+ <string name="search">Rechercher</string>
+ <string name="create_contact">Ajouter un contact</string>
+ <string name="join_conference">Rejoindre la conférence</string>
+ <string name="delete_contact">Supprimer le contact</string>
+ <string name="view_contact_details">Afficher les détails du contact</string>
+ <string name="create">Ajouter</string>
+ <string name="contact_already_exists">Le contact existe déjà.</string>
+ <string name="join">Rejoindre</string>
+ <string name="conference_address">Adresse de la conférence</string>
+ <string name="conference_address_example">salle@conference.exemple.com</string>
+ <string name="save_as_bookmark">Enregistrer en favoris</string>
+ <string name="delete_bookmark">Supprimer le favoris</string>
+ <string name="bookmark_already_exists">Ce favoris existe déjà.</string>
+ <string name="you">Vous</string>
+ <string name="action_edit_subject">Modifier le sujet de la conférence</string>
+ <string name="conference_not_found">Conférence non trouvée</string>
+ <string name="leave">Partir</string>
+ <string name="contact_added_you">Votre correspondant vous a ajouté dans sa liste de contacts</string>
+ <string name="add_back">Ajouter également</string>
+ <string name="contact_has_read_up_to_this_point">%s a lu les messages précédents.</string>
+ <string name="publish">Publier</string>
+ <string name="touch_to_choose_picture">Toucher l\'avatar pour choisir une image depuis la galerie.</string>
+ <string name="publish_avatar_explanation">Nota Bene: Les personnes ayant activé les mises jour de présence verront cette image.</string>
+ <string name="publishing">Mise à jour&#8230;</string>
+ <string name="error_publish_avatar_server_reject">Le serveur a rejeté votre envoi d\'image</string>
+ <string name="error_publish_avatar_converting">Une erreur s\'est produite pendant la conversion de votre image.</string>
+ <string name="error_saving_avatar">Impossible de stocker l\'image sur le disque</string>
+ <string name="or_long_press_for_default">(Un appui long réinitialise le paramètre par defaut)</string>
+ <string name="error_publish_avatar_no_server_support">Votre serveur n\'autorise pas l\'envoi d\'avatars</string>
+ <string name="private_message">chuchoté</string>
+ <string name="private_message_to">pour %s</string>
+ <string name="send_private_message_to">Envoyer un message privé à %s</string>
+ <string name="connect">Se connecter</string>
+ <string name="account_already_exists">Ce compte existe déjà</string>
+ <string name="next">suivant</string>
+ <string name="server_info_session_established">Session établie</string>
+ <string name="additional_information">Informations supplémentaires</string>
+ <string name="skip">Passer</string>
+ <string name="disable_notifications">Désactiver les notifications</string>
+ <string name="disable_notifications_for_this_conversation">Désactiver les notifications pour cette conversation</string>
+ <string name="notifications_disabled">Notifications are Désactivées</string>
+ <string name="enable">Activer</string>
+ <string name="conference_requires_password">La conférence necessite un mot de passe</string>
+ <string name="enter_password">Entrer le mot de passe</string>
+ <string name="missing_presence_updates">Mise à jour de présence non connue</string>
+ <string name="request_presence_updates">Merci de demander à votre contact de fournir les mises à jour de présence.\n\n<small>Cela permettra de savoir quel matériel utilise votre contact.</small></string>
+ <string name="request_now">Demander maintenant</string>
+ <string name="delete_fingerprint">Supprimer l\'empreinte</string>
+ <string name="sure_delete_fingerprint">Etes-vous sûr de vouloir supprimer l\'empreinte?</string>
+ <string name="ignore">Ignorer</string>
+ <string name="without_mutual_presence_updates"><b>Attention:</b> Ceci peut poser problème si l\'un des deux correspondants n\'a pas activé les mises à jour de présence.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string>
+ <string name="pref_encryption_settings">Paramètres de chiffrement</string>
+ <string name="pref_force_encryption">Forcer le chiffrement de bout en bout</string>
+ <string name="pref_force_encryption_summary">Toujours envoyer des messages chiffrés (sauf pour les conférences)</string>
+ <string name="pref_dont_save_encrypted">Ne pas sauvegarder les messages chiffrés</string>
+ <string name="pref_dont_save_encrypted_summary">Attention: Celà peut mener à une perte de messages</string>
+ <string name="pref_expert_options">Options avancées</string>
+ <string name="pref_expert_options_summary">A utiliser avec précautions</string>
+ <string name="pref_use_larger_font">Augmenter la taille du texte</string>
+ <string name="pref_use_larger_font_summary">Augmenter la taille du texte partout dans l\'application</string>
+ <string name="pref_use_send_button_to_indicate_status">Le bouton Envoyer permet d\'indiquer le statut</string>
+ <string name="pref_use_indicate_received">Accusé de reception</string>
+ <string name="pref_use_indicate_received_summary">Les messages recus seront marqués d\'une coche verte si disponible</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Adapter la couleur du bouton Envoyer pour indiquer le statut</string>
+ <string name="pref_expert_options_other">Autres</string>
+ <string name="pref_conference_name">Nom de la conférence </string>
+ <string name="pref_conference_name_summary">Identifier la conférence par son nom plutot que par son JID</string>
+ <string name="toast_message_otr_fingerprint">Empreinte OTR copiée dans le presse-papier!</string>
+ <string name="conference_banned">Vous êtes interdit de cette conférence</string>
+ <string name="conference_members_only">Cette conférence est réservée aux membres</string>
+ <string name="conference_kicked">Vous avez été éjecté de cette conférence</string>
+ <string name="using_account">utiliser le compte %s</string>
+ <string name="checking_image">Vérification de l\'image</string>
+ <string name="image_file_deleted">L\'image a été suprimée</string>
+ <string name="not_connected_try_again">Vous n\'êtes pas connecté. Merci de retenter plus tard.</string>
+
+</resources>
diff --git a/src/main/res/values-gl/arrays.xml b/src/main/res/values-gl/arrays.xml
new file mode 100644
index 00000000..19424a78
--- /dev/null
+++ b/src/main/res/values-gl/arrays.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Móvil</item>
+ <item>Teléfono</item>
+ <item>Tablet</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>nunca</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml
new file mode 100644
index 00000000..58116463
--- /dev/null
+++ b/src/main/res/values-gl/strings.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Axustes</string>
+ <string name="action_add">Nova conversa</string>
+ <string name="action_accounts">Xestionar contas</string>
+ <string name="action_end_conversation">Terminar conversa</string>
+ <string name="action_contact_details">Detalles do contacto</string>
+ <string name="action_muc_details">Detalles da conferencia</string>
+ <string name="action_secure">Conversa segura</string>
+ <string name="action_add_account">Engadir conta</string>
+ <string name="action_edit_contact">Editar contacto</string>
+ <string name="action_delete_contact">Eliminar contacto da lista</string>
+ <string name="just_now">agora</string>
+ <string name="minutes_ago">min</string>
+ <string name="unread_conversations">conversas sen ler</string>
+ <string name="sending">enviando&#8230;</string>
+ <string name="encrypted_message">Descifrando mensaxe. Agarda uns intres&#8230;</string>
+ <string name="nick_in_use">O apodo xa está en uso</string>
+ <string name="moderator">Moderador</string>
+ <string name="participant">Participante</string>
+ <string name="visitor">Visitante</string>
+ <string name="remove_contact_text">¿Queres eliminar a %s da túa lista?. A conversa asociada a esta conta non se eliminará.</string>
+ <string name="register_account">Rexistrar nova conta no servidor</string>
+ <string name="share_with">Compartir con</string>
+ <string name="start_conversation">Comeza conversa</string>
+ <string name="cancel">Cancelar</string>
+ <string name="crash_report_title">Conversations deteuse.</string>
+ <string name="crash_report_message">Enviando volcados de pilas axudas ao desenrolo de Conversations\n<b>Aviso:</b> Isto empregará a túa conta XMPP para enviar o volcado de pila ao desenrolador.</string>
+ <string name="send_now">Enviar agora</string>
+ <string name="send_never">Non preguntar de novo</string>
+ <string name="problem_connecting_to_account">Erro na conexión á conta</string>
+ <string name="problem_connecting_to_accounts">Erro na conexión a múltiples contas</string>
+ <string name="touch_to_fix">Pulsa aquí para xestionar as túas contass</string>
+ <string name="attach_file">Adxuntar</string>
+ <string name="not_in_roster">O contacto non está na túa lista. ¿Queres engadilo?</string>
+ <string name="add_contact">Engadir contacto</string>
+ <string name="send_failed">Erro ao enviar</string>
+ <string name="send_rejected">rechazado</string>
+ <string name="receiving_image">Recibindo arquivo de imaxe. Agarda por favor&#8230;</string>
+ <string name="preparing_image">Preparando imaxe para enviar</string>
+ <string name="action_clear_history">Limpar historial</string>
+ <string name="clear_conversation_history">Limpar historial de conversa</string>
+ <string name="clear_histor_msg">¿Queres borrar todas as mensaxes desta conversa?\n\n<b>Ollo:</b> Isto non afectará ás mensaxes gardadas noutros dispositivos ou servidores.</string>
+ <string name="delete_messages">Borrar mensaxes</string>
+ <string name="also_end_conversation">Terminar esta conversa máis tarde</string>
+ <string name="choose_presence">Selecciona recurso del contacto</string>
+ <string name="send_plain_text_message">Enviar mensaxe de texto</string>
+ <string name="send_otr_message">Enviar mensaxe cifrado con OTR</string>
+ <string name="send_pgp_message">Enviar mensaxe cifrado con OpenPGP</string>
+ <string name="your_nick_has_been_changed">Modificouse o teu apodo</string>
+ <string name="download_image">Descargar imaxe</string>
+ <string name="image_offered_for_download"><i>Arquivo de imaxe ofrecido para descarga</i></string>
+ <string name="send_unencrypted">Enviar sen cifrar</string>
+ <string name="decryption_failed">Fallou o descifrado. Quizábeis non teñas a clave privada apropiada.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations emprega unha aplicación de terceiros chamada <b>OpenKeychain</b> para cifrar e descifrar mensaxes e xestionar as túas claves públicas.\n\nOpenKeychain está publicado baixo licencia GPLv3 e disponible en F-Droid e Google Play.\n\n<small>(Por favor, reinicie Conversations despois.)</small></string>
+ <string name="restart">Reiniciar</string>
+ <string name="install">Instalar</string>
+ <string name="offering">ofrecendo&#8230;</string>
+ <string name="no_pgp_key">Clave OpenPGP non atopada</string>
+ <string name="contact_has_no_pgp_key">Conversations non foi quen de cifrar as túas mensaxes porque o teu contactos non está anunciando a súa clave pública.\n\n<small>Por favor, pídelle ao teu contacto que configure OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Mensaxe cifrado recibido. Pulsa para ver.</i></string>
+ <string name="encrypted_image_received"><i>Imaxe cifrada recibida. Pulsa para ver.</i></string>
+ <string name="image_file"><i>Imaxe recibida. Pulsa para ver</i></string>
+ <string name="pref_xmpp_resource">Recurso</string>
+ <string name="pref_xmpp_resource_summary">O nome que identifica o cliente que estás a empregar</string>
+ <string name="pref_accept_files">Aceptar arquivos</string>
+ <string name="pref_accept_files_summary">De forma automática aceptar arquivos menores de&#8230;</string>
+ <string name="pref_notification_settings">Axustes de notificación</string>
+ <string name="pref_notifications">Notificacións</string>
+ <string name="pref_notifications_summary">Notifica cuando chega unha nova mensaxe</string>
+ <string name="pref_vibrate">Tremer</string>
+ <string name="pref_vibrate_summary">Treme cando chega unha novo mensaxe</string>
+ <string name="pref_sound">Son</string>
+ <string name="pref_sound_summary">Reproduce un ton ca notificación</string>
+ <string name="pref_conference_notifications">Notificacións de conferencia</string>
+ <string name="pref_conference_notifications_summary">Siempre notifica cuando chega unha mensaxe de conferencia e non solo cuando chega unha mensaxe destacada</string>
+ <string name="pref_notification_grace_period">Notificacións Carbons</string>
+ <string name="pref_notification_grace_period_summary">Deshabilita as notificacións durante un corto periodo de tiempo despois de recibir a copia da mensaxe carbón</string>
+ <string name="pref_advanced_options">Opcións avanzadas</string>
+ <string name="pref_never_send_crash">Nunca enviar informe de erros</string>
+ <string name="pref_never_send_crash_summary">Enviando volcados de pilas axudas al desenrolo de Conversations</string>
+ <string name="openpgp_error">OpenKeychain reportou un erro</string>
+ <string name="error_decrypting_file">I/O Erro descifrando arquivo</string>
+ <string name="accept">Aceptar</string>
+ <string name="error">Produciuse un erro</string>
+ <string name="pref_grant_presence_updates">Suscripción de presencia</string>
+ <string name="pref_grant_presence_updates_summary">Por defecto otorgar e pedir suscripcións de presencia dos contactos que creaches</string>
+ <string name="subscriptions">Suscripcións</string>
+ <string name="your_account">A túa conta</string>
+ <string name="keys">Chaves</string>
+ <string name="send_presence_updates">Enviar actualizacións de presencia</string>
+ <string name="receive_presence_updates">Recibir actualizacións de presencia</string>
+ <string name="ask_for_presence_updates">Solicitar actualizacións de presencia</string>
+ <string name="attach_choose_picture">Seleccionar imaxe</string>
+ <string name="attach_take_picture">Facer foto</string>
+ <string name="preemptively_grant">Por defecto otorgar peticiones de suscripción</string>
+ <string name="error_not_an_image_file">O arquivo seleccionado non é unha imaxe</string>
+ <string name="error_compressing_image">Erro convertindo o arquivo de imaxe</string>
+ <string name="error_file_not_found">Arquivo non atopado</string>
+ <string name="error_io_exception">Erro xeral de I/O. ¿Quedaches sen espazo no disco?</string>
+ <string name="error_security_exception_during_image_copy">A aplicación que usas para seleccionar imaxes non proporciona suficientes permisos para leer o arquivo.\n\n<small>Utiliza un explorador de arquivos diferente para seleccionar a imaxe</small></string>
+ <string name="account_status_unknown">Descoñecido</string>
+ <string name="account_status_disabled">Deshabilitado temporalmente</string>
+ <string name="account_status_online">Conectado</string>
+ <string name="account_status_connecting">Conectando\u2026</string>
+ <string name="account_status_offline">Desconectado</string>
+ <string name="account_status_unauthorized">Non autorizado</string>
+ <string name="account_status_not_found">Servidor non atopado</string>
+ <string name="account_status_no_internet">Sen conectividade</string>
+ <string name="account_status_regis_fail">Erro no rexistro</string>
+ <string name="account_status_regis_conflict">O identificador xa está en uso</string>
+ <string name="account_status_regis_success">Rexistro completado</string>
+ <string name="account_status_regis_not_sup">O servidor non soporta rexistros</string>
+ <string name="encryption_choice_none">Texto plano</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Editar conta</string>
+ <string name="mgmt_account_delete">Eliminar conta</string>
+ <string name="mgmt_account_disable">Deshabilitar temporalmente</string>
+ <string name="mgmt_account_enable">Habilitar</string>
+ <string name="attach_record_voice">Grabar audio</string>
+ <string name="save">Gardar</string>
+ <string name="passwords_do_not_match">As contrasinais non coinciden</string>
+ <string name="invalid_jid">O identificador non é un identificador de Jabber válido</string>
+ <string name="pref_ui_options">Opcións de interfaz</string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-it/arrays.xml b/src/main/res/values-it/arrays.xml
new file mode 100644
index 00000000..491c4438
--- /dev/null
+++ b/src/main/res/values-it/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Cellulare</item>
+ <item>Telefono</item>
+ <item>Tablet</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>mai</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 minuti</item>
+ <item>un\'ora</item>
+ <item>2 ore</item>
+ <item>8 ore</item>
+ <item>fino avviso ulteriore</item>
+ </string-array>
+
+ <integer-array name="mute_options_durations">
+ <item>1800</item>
+ <item>3600</item>
+ <item>7200</item>
+ <item>28800</item>
+ <item>-1</item>
+ </integer-array>
+
+</resources>
diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml
new file mode 100644
index 00000000..0bb0e05e
--- /dev/null
+++ b/src/main/res/values-it/strings.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Impostazioni</string>
+ <string name="action_add">Nuova conversazione</string>
+ <string name="action_accounts">Gestisci utenti</string>
+ <string name="action_end_conversation">Termina questa conversazione</string>
+ <string name="action_contact_details">Dettagli del contatto</string>
+ <string name="action_muc_details">Dettagli conferenza</string>
+ <string name="action_secure">Conversazione sicura</string>
+ <string name="action_add_account">Aggiungi utente</string>
+ <string name="action_edit_contact">Modifica il nome</string>
+ <string name="action_add_phone_book">Aggiungi alla rubrica</string>
+ <string name="action_delete_contact">Cancella dalla lista</string>
+ <string name="title_activity_manage_accounts">Gestisci Utenti</string>
+ <string name="title_activity_settings">Impostazioni</string>
+ <string name="title_activity_conference_details">Dettagli conferenza</string>
+ <string name="title_activity_contact_details">Dettagli del contatto</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">Condividi con Conversation</string>
+ <string name="title_activity_start_conversation">Inizia una Conversazione</string>
+ <string name="title_activity_choose_contact">Scegli contatto</string>
+ <string name="just_now">adesso</string>
+ <string name="minute_ago">1 min fa</string>
+ <string name="minutes_ago">%d min fa</string>
+ <string name="unread_conversations">Conversazioni non lette</string>
+ <string name="sending">invio&#8230;</string>
+ <string name="encrypted_message">Decifrazione del messaggio. Attendere prego&#8230;</string>
+ <string name="nick_in_use">Nome utente già in uso</string>
+ <string name="admin">Amministratore</string>
+ <string name="owner">Proprietario</string>
+ <string name="moderator">Moderatore</string>
+ <string name="participant">Partecipante</string>
+ <string name="visitor">Visitatore</string>
+ <string name="remove_contact_text">Vuoi rimuovere %s dalla tua lista contatti? La conversazione associata con questo contatto non sarà rimossa.</string>
+ <string name="remove_bookmark_text">Vuoi rimuovere il segnalibro %s? La conversazione associata con questo contatto non sarà rimossa.</string>
+ <string name="register_account">Registra un nuovo account sul server</string>
+ <string name="share_with">Condividi con</string>
+ <string name="start_conversation">Inizia Conversazione</string>
+ <string name="invite_contact">Invita Contatto</string>
+ <string name="contacts">Contatti</string>
+ <string name="cancel">Cancella</string>
+ <string name="add">Aggiungi</string>
+ <string name="edit">Modifica</string>
+ <string name="delete">Elimina</string>
+ <string name="save">Salva</string>
+ <string name="ok">OK</string>
+ <string name="crash_report_title">Conversations è crashato</string>
+ <string name="crash_report_message">Se scegli di inviare una segnalazione dell\'errore aiuterai lo sviluppo di Conversations\n<b>Attenzione:</b> Questo utilizzerà il tuo account XMPP per inviare la segnalazione agli sviluppatori.</string>
+ <string name="send_now">Invia adesso</string>
+ <string name="send_never">Non chiedere mai più</string>
+ <string name="problem_connecting_to_account">Impossibile collegarsi all\'utente</string>
+ <string name="problem_connecting_to_accounts">Impossibile collegarsi a più utenti</string>
+ <string name="touch_to_fix">Tocca qui per gestire i tuoi utenti</string>
+ <string name="attach_file">Allega file</string>
+ <string name="not_in_roster">Il contatto non è nella tua lista. Vuoi aggiungerlo?</string>
+ <string name="add_contact">Aggiungi contatto</string>
+ <string name="send_failed">Invio fallito</string>
+ <string name="send_rejected">rifiutato</string>
+ <string name="receiving_image">Ricezione di un\'immagine. Attendere prego&#8230;</string>
+ <string name="preparing_image">Preparazioone immagine per la trasmissione</string>
+ <string name="action_clear_history">Pulisci la cronologia</string>
+ <string name="clear_conversation_history">Pulisci la cronologia della Conversazione</string>
+ <string name="clear_histor_msg">Vuoi cancellare tutti i messaggi di questa Conversazione?\n\n<b>Attenzione:</b> Questo non influenzerà i messaggi presenti su altri dispositivi o server.</string>
+ <string name="delete_messages">Elimina messaggi</string>
+ <string name="also_end_conversation">Termina questa conversazione in seguito</string>
+ <string name="choose_presence">Choose presence to contact</string>
+ <string name="send_plain_text_message">Invia messaggio di testo semplice</string>
+ <string name="send_otr_message">Invia messaggio cifrato con OTR</string>
+ <string name="send_pgp_message">Invia messaggio cifrato con OpenPGP</string>
+ <string name="your_nick_has_been_changed">Il tuo nome utente èstato cambiato</string>
+ <string name="download_image">Scarica Immagine</string>
+ <string name="image_offered_for_download"><i>Immagine disponibile per il download</i></string>
+ <string name="send_unencrypted">Invia non cifrato</string>
+ <string name="decryption_failed">Decifrazione fallita. Forse non disponi della chiave privata corretta.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations usa una app di terze parti chiamata <b>OpenKeychain</b> per cifrare e decifrare i messaggi per gestire le tue chiavi pubbliche.\n\nOpenKeychain è rilasciato secondo i termini della GPLv3 ed è disponibile sia su F-Droid, che su Google Play.\n\n<small>(Riavvia Conversations in seguito.)</small></string>
+ <string name="restart">Riavvia</string>
+ <string name="install">Installa</string>
+ <string name="offering">offrendo&#8230;</string>
+ <string name="waiting">in attesa&#8230;</string>
+ <string name="no_pgp_key">Nessuna chiave OpenPGP trovata</string>
+ <string name="contact_has_no_pgp_key">Conversations non è in grado di cifrare i tuoi messaggi perchè il contatto non sta annunciando la sua chiave pubblica.\n\n<small>Per favore chiedi al tuo contatto di configurare OpenPGP.</small></string>
+ <string name="no_pgp_keys">Nessuna chiave OpenPGP trovata</string>
+ <string name="contacts_have_no_pgp_keys">Conversations non è in grado di cifrare i tuoi messaggi perchè i contatti non stanno annunciando la propria chiave pubblica.\n\n<small>Per favore chiedi ai tuoi contatti di configurare OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Messaggio cifrato ricevuto. Tocca per decifrare.</i></string>
+ <string name="encrypted_image_received"><i>Immagine cifrata ricevuta. Tocca per decifrare e mostrare.</i></string>
+ <string name="image_file"><i>Immagine ricevuta. Tocca per mostrare</i></string>
+ <string name="pref_general">Generale</string>
+ <string name="pref_xmpp_resource">Risorsa XMPP</string>
+ <string name="pref_xmpp_resource_summary">Il nome con il quale questo client si identifica</string>
+ <string name="pref_accept_files">Accetta i file</string>
+ <string name="pref_accept_files_summary">Accetta automaticamente i file più piccoli di&#8230;</string>
+ <string name="pref_notification_settings">Impostazioni di Notifica</string>
+ <string name="pref_notifications">Notifiche</string>
+ <string name="pref_notifications_summary">Notifica quando arriva un nuovo messaggio</string>
+ <string name="pref_vibrate">Vibra</string>
+ <string name="pref_vibrate_summary">Vibra anche quando arriva un nuovo messaggio</string>
+ <string name="pref_sound">Suono</string>
+ <string name="pref_sound_summary">Riproduci una suoneria con la notifica</string>
+ <string name="pref_conference_notifications">Notifiche Conferenze</string>
+ <string name="pref_conference_notifications_summary">Notifica sempre quando arriva un nuovo messaggio da una conferenza, invece che solo quando in primo piano</string>
+ <string name="pref_notification_grace_period">Periodo tra notifiche</string>
+ <string name="pref_notification_grace_period_summary">Disabilita le notifiche per un breve lasso di tempo dopo che un messaggio è stato ricevuto</string>
+ <string name="pref_advanced_options">Opzioni Avanzate</string>
+ <string name="pref_never_send_crash">Non inviare mai segnalazioni di errore</string>
+ <string name="pref_never_send_crash_summary">Se scegli di inviare una segnalazione dell\'errore aiuterai lo sviluppo di Conversations</string>
+ <string name="pref_confirm_messages">Conferma Messaggi</string>
+ <string name="pref_confirm_messages_summary">Fai sapere ai tuoi contatti quando hai ricevuto il messaggio e l\'hai letto</string>
+ <string name="pref_ui_options">Opzioni Interfaccia</string>
+ <string name="openpgp_error">OpenKeychain ha riportato un errore</string>
+ <string name="error_decrypting_file">Errore di I/O nel decifrare il file</string>
+ <string name="accept">Accetta</string>
+ <string name="error">Si è verificato un errore</string>
+ <string name="pref_grant_presence_updates">Concedi aggiornamenti della presenza</string>
+ <string name="pref_grant_presence_updates_summary">Concedi e chiedi preventivamente la sottoscrizione della presenza ai contatti che hai creato</string>
+ <string name="subscriptions">Sottoscrizioni</string>
+ <string name="your_account">Il tuo utente</string>
+ <string name="keys">Chiavi</string>
+ <string name="send_presence_updates">Invia aggiornamenti della presenza</string>
+ <string name="receive_presence_updates">Ricevi aggiornamenti della presenza</string>
+ <string name="ask_for_presence_updates">Chiedi aggiornamenti della presenza</string>
+ <string name="attach_choose_picture">Scegli un\'immagine</string>
+ <string name="attach_take_picture">Foto</string>
+ <string name="preemptively_grant">Concedi aggiornamenti della presenza preventivamente</string>
+ <string name="error_not_an_image_file">Il file selezionato non è un\'immagine</string>
+ <string name="error_compressing_image">Errore durante la conversione dell\'immagine</string>
+ <string name="error_file_not_found">File non trovato</string>
+ <string name="error_io_exception">Errore di I/O generico. Forse hai esaurito lo spazio?</string>
+ <string name="error_security_exception_during_image_copy">L\'app che hai usato per selezionare questa immagine non ci ha fornito permessi sufficienti per leggere il file.\n\n<small>Usa un file manager differente per scegliere un\'immagine</small></string>
+ <string name="account_status_unknown">Sconosciuto</string>
+ <string name="account_status_disabled">Disabilitato temporaneamente</string>
+ <string name="account_status_online">Online</string>
+ <string name="account_status_connecting">In connessione\u2026</string>
+ <string name="account_status_offline">Offline</string>
+ <string name="account_status_unauthorized">Non autorizzato</string>
+ <string name="account_status_not_found">Server non trovato</string>
+ <string name="account_status_no_internet">Connettività assente</string>
+ <string name="account_status_regis_fail">Registrazione fallita</string>
+ <string name="account_status_regis_conflict">Nome utente già in uso</string>
+ <string name="account_status_regis_success">Registrazione completata</string>
+ <string name="account_status_regis_not_sup">Il Server non supporta la registrazione</string>
+ <string name="encryption_choice_none">Testo semplice</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Modifica utente</string>
+ <string name="mgmt_account_delete">Elimina utente</string>
+ <string name="mgmt_account_disable">Disabilita temporaneamente</string>
+ <string name="mgmt_account_publish_avatar">Pubblica avatar</string>
+ <string name="mgmt_account_publish_pgp">Pubblica chiave pubblica OpenPGP</string>
+ <string name="mgmt_account_enable">Abilita utente</string>
+ <string name="mgmt_account_are_you_sure">Sei sicuro?</string>
+ <string name="mgmt_account_delete_confirm_text">Se cancelli il tuo utente la cronologia delle tue conversazioni verrà persa</string>
+ <string name="attach_record_voice">Registra la voce</string>
+ <string name="account_settings_jabber_id">ID Jabber</string>
+ <string name="account_settings_password">Password</string>
+ <string name="account_settings_example_jabber_id">utente@esempio.com</string>
+ <string name="account_settings_confirm_password">Conferma password</string>
+ <string name="password">Password</string>
+ <string name="confirm_password">Conferma password</string>
+ <string name="passwords_do_not_match">Le Password non corrispondono</string>
+ <string name="invalid_jid">Questo non è un ID Jabber valido</string>
+ <string name="error_out_of_memory">Memoria esaurita. L\'immagine è tropppo grande</string>
+ <string name="add_phone_book_text">Vuoi aggiungere %s alla rubrica del telefono?</string>
+ <string name="contact_status_online">online</string>
+ <string name="contact_status_free_to_chat">vuole chattare</string>
+ <string name="contact_status_away">assente</string>
+ <string name="contact_status_extended_away">assenza prolungata</string>
+ <string name="contact_status_do_not_disturb">non disturbare</string>
+ <string name="contact_status_offline">offline</string>
+ <string name="muc_details_conference">Conferenza</string>
+ <string name="muc_details_other_members">Altri Membri</string>
+ <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+ <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+ <string name="server_info_available">disponibile</string>
+ <string name="server_info_unavailable">non disponibile</string>
+ <string name="missing_public_keys">Annuncio chiave pubblica non effettuato</string>
+ <string name="last_seen_now">visto adesso</string>
+ <string name="last_seen_min">visto 1 minuto fa</string>
+ <string name="last_seen_mins">visto %d minuti fa</string>
+ <string name="last_seen_hour">visto 1 ora fa</string>
+ <string name="last_seen_hours">visto %d ore fa</string>
+ <string name="last_seen_day">visto 1 giorno fa</string>
+ <string name="last_seen_days">visto %d giorni fa</string>
+ <string name="never_seen">mai visto</string>
+ <string name="install_openkeychain">Messaggio cifrato. Installa OpenKeychain per decifrare.</string>
+ <string name="unknown_otr_fingerprint">Impronta OTR sconosciuta</string>
+ <string name="openpgp_messages_found">Messaggi cifrati con OpenPGP trovati</string>
+ <string name="reception_failed">Ricezione fallita</string>
+ <string name="your_fingerprint">La tua impronta</string>
+ <string name="otr_fingerprint">Impronta OTR</string>
+ <string name="verify">Verifica</string>
+ <string name="decrypt">Decripta</string>
+ <string name="conferences">Conferenze</string>
+ <string name="search">Cerca</string>
+ <string name="create_contact">Crea Contatto</string>
+ <string name="join_conference">Entra in Conferenza</string>
+ <string name="delete_contact">Elimina Contatto</string>
+ <string name="view_contact_details">Mostra dettagli contatto</string>
+ <string name="create">Crea</string>
+ <string name="contact_already_exists">Il contatto esiste già</string>
+ <string name="join">Entra</string>
+ <string name="conference_address">Indirizzo conferenza</string>
+ <string name="conference_address_example">room@conference.example.com</string>
+ <string name="save_as_bookmark">Salva come segnalibro</string>
+ <string name="delete_bookmark">Elimina segnalibro</string>
+ <string name="bookmark_already_exists">Questo segnalibro esiste già</string>
+ <string name="you">Tu</string>
+ <string name="action_edit_subject">Modifica soggetto conferenza</string>
+ <string name="conference_not_found">Conferenza non trovata</string>
+ <string name="leave">Abbandona</string>
+ <string name="contact_added_you">Il contatto ti ha aggiunto alla sua lista contatti</string>
+ <string name="add_back">Add back</string>
+ <string name="contact_has_read_up_to_this_point">%s ha letto fino a questo punto</string>
+ <string name="publish">Pubblica</string>
+ <string name="touch_to_choose_picture">Tocca l\'avatar per selezionare l\'immagine dalla gallaria</string>
+ <string name="publish_avatar_explanation">Nota bene: tutti i contatti sottoscritti agli aggiornamenti della tua presenza avranno il permesso di vedere questa immagine.</string>
+ <string name="publishing">Pubblicazione&#8230;</string>
+ <string name="error_publish_avatar_server_reject">Il server ha rifiutato la tua pubblicazione</string>
+ <string name="error_publish_avatar_converting">Qualcosa è andato storto durante la conversione della tua immagine</string>
+ <string name="error_saving_avatar">Impossibile salvare l\'avatar sulla memoria interna</string>
+ <string name="or_long_press_for_default">(O premi a lungo per ripristinare le impostazioni di default)</string>
+ <string name="error_publish_avatar_no_server_support">Il tuo server non supporta la pubblicazione degli avatar</string>
+ <string name="private_message">sussurrato</string>
+ <string name="private_message_to">a %s</string>
+ <string name="send_private_message_to">Invia messaggio privato a %s</string>
+ <string name="connect">Connetti</string>
+ <string name="account_already_exists">Questo utente esiste già</string>
+ <string name="next">Successivo</string>
+ <string name="server_info_session_established">Sessione corrente stabilita</string>
+ <string name="additional_information">Informazioni Aggiuntive</string>
+ <string name="skip">Salta</string>
+ <string name="disable_notifications">Disabilita le notifiche</string>
+ <string name="disable_notifications_for_this_conversation">Disabilita le notifiche per questa conversazione</string>
+ <string name="notifications_disabled">Le notifiche sono disabilitate</string>
+ <string name="enable">Abilita</string>
+ <string name="conference_requires_password">La conferenza richiede una password</string>
+ <string name="enter_password">Inserisci la password</string>
+ <string name="missing_presence_updates">Aggiornamenti della presenza del contatto mancanti</string>
+ <string name="request_presence_updates">Richiedi gli aggiornamenti della presenza dal tuo contatto.\n\n<small>Questo verrà usato per determinare quali client sta usando il tuo contatto.</small></string>
+ <string name="request_now">Rechiedi adesso</string>
+ <string name="delete_fingerprint">Elimina Impronta</string>
+ <string name="sure_delete_fingerprint">Sei sicuro di voler eliminare questa impronta?</string>
+ <string name="ignore">Ignora</string>
+ <string name="without_mutual_presence_updates"><b>Attenzione:</b> Inviando questo messaggio senza aggiornamenti della presenza reciproci potrebbe causare problemi inaspettati.\n\n<small>Vai nei dettagli del contatto per verificare le tue sottoscrizioni alla presenza.</small></string>
+ <string name="pref_encryption_settings">Impostazioni di cifratura</string>
+ <string name="pref_force_encryption">Forza cifratura end-to-end</string>
+ <string name="pref_force_encryption_summary">Manda sempre messaggi cifrati (ad eccezione delle conferenze)</string>
+ <string name="pref_dont_save_encrypted">Non salvare i messaggi cifrati</string>
+ <string name="pref_dont_save_encrypted_summary">Attenzione: Questo potrebbe comportare la perdita di messaggi</string>
+ <string name="pref_expert_options">Opzioni da Esperto</string>
+ <string name="pref_expert_options_summary">Fai attenzione con queste impostazioni</string>
+ <string name="pref_use_larger_font">Aumenta la dimensione dei font</string>
+ <string name="pref_use_larger_font_summary">Usa font più grandi in tutta l\'app</string>
+ <string name="pref_use_send_button_to_indicate_status">Il pulsante di invio indica lo stato</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Colora il pulsante di invio per indicare lo stato di un contatto</string>
+
+</resources>
diff --git a/src/main/res/values-iw/arrays.xml b/src/main/res/values-iw/arrays.xml
new file mode 100644
index 00000000..28768d6c
--- /dev/null
+++ b/src/main/res/values-iw/arrays.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>נייד</item>
+ <item>טלפון</item>
+ <item>טאבלט</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>אף פעם</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml
new file mode 100644
index 00000000..fd8eaa0b
--- /dev/null
+++ b/src/main/res/values-iw/strings.xml
@@ -0,0 +1,224 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">הגדרות</string>
+ <string name="action_add">דיון חדש</string>
+ <string name="action_accounts">נהל חשבונות</string>
+ <string name="action_end_conversation">סיים את דיון זה</string>
+ <string name="action_contact_details">פרטי איש קשר</string>
+ <string name="action_muc_details">פרטי ועידה</string>
+ <string name="action_secure">דיון מאובטח</string>
+ <string name="action_add_account">הוסף חשבון</string>
+ <string name="action_edit_contact">ערוך שם</string>
+ <string name="action_add_phone_book">הוסף אל פנקס טלפונים</string>
+ <string name="action_delete_contact">מחק מתוך רשימה</string>
+ <string name="title_activity_manage_accounts">נהל חשבונות</string>
+ <string name="title_activity_settings">הגדרות</string>
+ <string name="title_activity_conference_details">פרטי ועידה</string>
+ <string name="title_activity_contact_details">פרטי איש קשר</string>
+ <string name="title_activity_conversations">דיונים</string>
+ <string name="title_activity_sharewith">שתף בעזרת Conversations</string>
+ <string name="title_activity_start_conversation">התחל דיון</string>
+ <string name="title_activity_choose_contact">בחר איש קשר</string>
+ <string name="just_now">רק כעת</string>
+ <string name="minute_ago">לפני דקה 1</string>
+ <string name="minutes_ago">לפני %d דקות</string>
+ <string name="unread_conversations">דיונים שלא נקראו</string>
+ <string name="sending">כעת שולח&#8230;</string>
+ <string name="encrypted_message">כעת מפענח הודעה. אנא המתן&#8230;</string>
+ <string name="nick_in_use">שם כינוי כבר מצוי בשימוש</string>
+ <string name="admin">מנהל</string>
+ <string name="owner">בעלים</string>
+ <string name="moderator">אחראי</string>
+ <string name="participant">משתתף</string>
+ <string name="visitor">מבקר</string>
+ <string name="remove_contact_text">האם ברצונך להסיר את %s מתןך הרשימה שלך? הדיונים אשר משוייכים עם חשבון זה לא יוסרו.</string>
+ <string name="remove_bookmark_text">האם ברצונך להסיר את %s בתוור סימנייה? הדיונים אשר משוייכים עם סימנייה זו לא יוסרו.</string>
+ <string name="register_account">רשום חשבון חדש על שרת</string>
+ <string name="share_with">שתף בעזרת</string>
+ <string name="start_conversation">התחל דיון</string>
+ <string name="invite_contact">הזמן איש קשר</string>
+ <string name="contacts">אנשי קשר</string>
+ <string name="cancel">ביטול</string>
+ <string name="add">הוסף</string>
+ <string name="edit">ערוך</string>
+ <string name="delete">מחק</string>
+ <string name="save">שמור</string>
+ <string name="ok">אישור</string>
+ <string name="crash_report_title">Conversations קרסה</string>
+ <string name="crash_report_message">על ידי שליחת עקבות מחסנית אתה עוזר להתקדמות הפיתוח של Conversations\n<b>אזהרה:</b> זו תעשה שימוש בחשבון XMPP שלך כדי לשלוח עקבות מחסנית אל המפתח.</string>
+ <string name="send_now">שלח עכשיו</string>
+ <string name="send_never">לעולם אל תשאל שוב</string>
+ <string name="problem_connecting_to_account">לא מסוגל להתחבר אל חשבון</string>
+ <string name="problem_connecting_to_accounts">לא מסוגל להתחבר אל חשבונות מרובים</string>
+ <string name="touch_to_fix">לחץ כאן כדי לנהל את החשבונות שלך</string>
+ <string name="attach_file">צרף קובץ</string>
+ <string name="not_in_roster">איש קשר אינו מצוי בתוך הרשימה שלך. האם ברצונך להוסיפו?</string>
+ <string name="add_contact">הוסף איש קשר</string>
+ <string name="send_failed">מסירה נכשלה</string>
+ <string name="send_rejected">סורב</string>
+ <string name="receiving_image">כעת מקבל קובץ תצלום. אנא המתן&#8230;</string>
+ <string name="preparing_image">כעת מכין תצלום לשם תמסורת</string>
+ <string name="action_clear_history">טהר היסטוריה</string>
+ <string name="clear_conversation_history">טהר היסטוריית דיונים</string>
+ <string name="clear_histor_msg">האם ברצונך למחוק את כל ההודעות בתוך דיון זה?\n\n<b>אזהרה:</b> זו לא תשפיע על הודעות מאוחסנות על מכשירים או שרתים אחרים.</string>
+ <string name="delete_messages">מחק הודעות</string>
+ <string name="also_end_conversation">סיים את דיון זה לאחר מכן</string>
+ <string name="choose_presence">בחר נוכחות לאיש קשר</string>
+ <string name="send_plain_text_message">שלח הודעת טקסט גלוי</string>
+ <string name="send_otr_message">שלח הודעה מוצפנת OTR</string>
+ <string name="send_pgp_message">שלח הודעה מוצפנת OpenPGP</string>
+ <string name="your_nick_has_been_changed">שם כינוי שלך השתנה</string>
+ <string name="download_image">הורד תצלום</string>
+ <string name="image_offered_for_download"><i>קובץ תצלום מוצע להורדה</i></string>
+ <string name="send_unencrypted">שלח לא מוצפנת</string>
+ <string name="decryption_failed">פענוח נכשל. אולי אין לך את המפתח הפרטי המתאים.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations מפיקה תועלת מן אפליקציית צד-שלישי הקרויה <b>OpenKeychain</b> כדי להצפין ולפענח הודעות וגם כדי לנהל את המפתחות הפומביים שלך.\n\nOpenKeychain הינה רשויה תחת GPLv3 וזמינה אצל F-Droid וגם Google Play.\n\n<small>(אנא התחל מחדש את Conversations לאחר מכן.)</small></string>
+ <string name="restart">התחל מחדש</string>
+ <string name="install">התקן</string>
+ <string name="offering">כעת מציע&#8230;</string>
+ <string name="waiting">כעת ממתין&#8230;</string>
+ <string name="no_pgp_key">לא נמצא מפתח OpenPGP</string>
+ <string name="contact_has_no_pgp_key">Conversations אינה מסוגלת להצפין את הודעותיך משום שאיש הקשר שלך אינו מכריז על המפתח הפומבי שלו או שלה.\n\n<small>אנא בקש מאיש הקשר שלך לארגן OpenPGP.</small></string>
+ <string name="no_pgp_keys">לא נמצאו מפתחות OpenPGP</string>
+ <string name="contacts_have_no_pgp_keys">Conversations אינה מסוגלת להצפין את הודעותיך משום שאנשי הקשר שלך אינם מכריזים על המפתח הפומבי שלהם.\n\n<small>אנא בקש מאנשי הקשר שלך לארגן OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>הודעה מוצפנת התקבלה. לחץ כדי לצפות ולפענח.</i></string>
+ <string name="encrypted_image_received"><i>תצלום מוצפן התקבל. לחץ כדי לצפות ולפענח.</i></string>
+ <string name="image_file"><i>תצלום התקבל. לחץ כדי לצפות</i></string>
+ <string name="pref_xmpp_resource">משאב XMPP</string>
+ <string name="pref_xmpp_resource_summary">השם שלקוח זה מזהה את עצמו עם</string>
+ <string name="pref_accept_files">קבל קבצים</string>
+ <string name="pref_accept_files_summary">קבל אוטומטית קבצים קטנים יותר מאשר&#8230;</string>
+ <string name="pref_notification_settings">הגדרות התראה</string>
+ <string name="pref_notifications">התראות</string>
+ <string name="pref_notifications_summary">תודיע כאשר הודעה חדשה מגיעה</string>
+ <string name="pref_vibrate">הרטט</string>
+ <string name="pref_vibrate_summary">הרטט גם כאשר הודעה חדשה מגיעה</string>
+ <string name="pref_sound">צליל</string>
+ <string name="pref_sound_summary">נגן צלצול עם התראה</string>
+ <string name="pref_conference_notifications">התראות ועידה</string>
+ <string name="pref_conference_notifications_summary">תמיד תודיע כאשר הודעת ועידה חדשה מגיעה במקום רק כאשר מודגשת</string>
+ <string name="pref_notification_grace_period">משך ארכת התראה</string>
+ <string name="pref_notification_grace_period_summary">נטרל התראות לזמן קצר לאחר שהודעת פחם התקבלה</string>
+ <string name="pref_advanced_options">אפשרויות מתקדמות</string>
+ <string name="pref_never_send_crash">לעולם אל תשלח דיווחי קריסה</string>
+ <string name="pref_never_send_crash_summary">על ידי שליחת עקבות מחסנית אתה עוזר להתקדמות הפיתוח של Conversations</string>
+ <string name="pref_confirm_messages">אשר הודעות</string>
+ <string name="pref_confirm_messages_summary">אפשר לאיש קשר שלך לדעת מתי קיבלת וקראת הודעה</string>
+ <string name="openpgp_error">OpenKeychain דיווח שגיאה</string>
+ <string name="error_decrypting_file">שגיאת I/O פענוח קובץ</string>
+ <string name="accept">קבל</string>
+ <string name="error">אירעה שגיאה</string>
+ <string name="pref_grant_presence_updates">הענק עדכוני נוכחות</string>
+ <string name="pref_grant_presence_updates_summary">הענק ובקש הרשמות נוכחות מראש עבור אנשי קשר שיצרת</string>
+ <string name="subscriptions">הרשמות</string>
+ <string name="your_account">החשבון שלך</string>
+ <string name="keys">מפתחות</string>
+ <string name="send_presence_updates">שלח עדכוני נוכחות</string>
+ <string name="receive_presence_updates">קבל עדכוני נוכחות</string>
+ <string name="ask_for_presence_updates">בקש עדכוני נוכחות</string>
+ <string name="attach_choose_picture">בחר תמונה</string>
+ <string name="attach_take_picture">קח תמונה</string>
+ <string name="preemptively_grant">הענק בקשת הרשמה מראש</string>
+ <string name="error_not_an_image_file">הקובץ שבחרת אינו תצלום</string>
+ <string name="error_compressing_image">שגיאה במהלך המרת קובץ תצלום</string>
+ <string name="error_file_not_found">קובץ לא נמצא</string>
+ <string name="error_io_exception">שגיאת I/O כללית. אולי אזל לך נפח אחסון?</string>
+ <string name="error_security_exception_during_image_copy">האפליקציה בה השתמשת כדי לבחור את תצלום זה לא סיפקה לנו מספיק הרשאות כדי לקרוא את הקובץ.\n\n<small>השתמש במנהל קבצים אחר כדי לבחור תצלום</small></string>
+ <string name="account_status_unknown">לא ידוע</string>
+ <string name="account_status_disabled">מנוטרל זמנית</string>
+ <string name="account_status_online">מקוון</string>
+ <string name="account_status_connecting">כעת מתחבר\u2026</string>
+ <string name="account_status_offline">לא מקוון</string>
+ <string name="account_status_unauthorized">לא מורשה</string>
+ <string name="account_status_not_found">שרת לא נמצא</string>
+ <string name="account_status_no_internet">אין חיבוריות</string>
+ <string name="account_status_regis_fail">הרשמה נכשלה</string>
+ <string name="account_status_regis_conflict">שם משתמש כבר מצוי בשימוש</string>
+ <string name="account_status_regis_success">הרשמה הושלמה</string>
+ <string name="account_status_regis_not_sup">שרת לא תומך הרשמה</string>
+ <string name="encryption_choice_none">טקסט גלוי</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">ערוך חשבון</string>
+ <string name="mgmt_account_delete">מחק</string>
+ <string name="mgmt_account_disable">נטרל זמנית</string>
+ <string name="mgmt_account_publish_avatar">פרסם אווטאר</string>
+ <string name="mgmt_account_enable">אפשר</string>
+ <string name="mgmt_account_are_you_sure">האם אתה בטוח?</string>
+ <string name="mgmt_account_delete_confirm_text">אם אתה מוחק את חשבונך כל היסטוריית הדיון שלך תאבד</string>
+ <string name="attach_record_voice">הקלט קול</string>
+ <string name="account_settings_jabber_id">מזהה Jabber</string>
+ <string name="account_settings_password">סיסמה</string>
+ <string name="account_settings_example_jabber_id">username@example.com</string>
+ <string name="account_settings_confirm_password">אמת סיסמה</string>
+ <string name="password">סיסמה</string>
+ <string name="confirm_password">אמת סיסמה</string>
+ <string name="passwords_do_not_match">סיסמאות לא תואמות</string>
+ <string name="invalid_jid">זה אינו מזהה Jabber תקף</string>
+ <string name="error_out_of_memory">חסר זיכרון. תצלום גדול מדי</string>
+ <string name="add_phone_book_text">האם ברצונך להוסיף את %s אל רשימת קשר טלפונית?</string>
+ <string name="contact_status_online">מקוון</string>
+ <string name="contact_status_free_to_chat">חופשי לשיחה</string>
+ <string name="contact_status_away">נעדר</string>
+ <string name="contact_status_extended_away">נעדר לזמן מה</string>
+ <string name="contact_status_do_not_disturb">אל תפריעו</string>
+ <string name="contact_status_offline">לא מקוון</string>
+ <string name="muc_details_conference">ועידה</string>
+ <string name="muc_details_other_members">חברים אחרים</string>
+ <string name="server_info_carbon_messages">הודעות פחם</string>
+ <string name="server_info_stream_management">ניהול זרם</string>
+ <string name="missing_public_keys">הכרזות מפתח פומבי חסרות</string>
+ <string name="last_seen_now">נראה לאחרונה ממש עכשיו</string>
+ <string name="last_seen_min">נראה לאחרונה לפני דקה 1</string>
+ <string name="last_seen_mins">נראה לאחרונה לפני %d דקות</string>
+ <string name="last_seen_hour">נראה לאחרונה לפני שעה 1</string>
+ <string name="last_seen_hours">נראה לאחרונה לפני %d שעות ago</string>
+ <string name="last_seen_day">נראה לאחרונה לפני יום 1</string>
+ <string name="last_seen_days">נראה לאחרונה לפני %d ימים</string>
+ <string name="never_seen">לא נראה מעולם</string>
+ <string name="install_openkeychain">הודעה מוצפנת. אנא התקן OpenKeychain כדי לפענח.</string>
+ <string name="unknown_otr_fingerprint">טביעת אצבע OTR לא מוכרת</string>
+ <string name="openpgp_messages_found">הודעות מוצפנות OpenPGP נמצאו</string>
+ <string name="reception_failed">קבלה נכשלה</string>
+ <string name="your_fingerprint">טביעת אצבע שלך</string>
+ <string name="otr_fingerprint">טביעת אצבע OTR</string>
+ <string name="verify">אמת</string>
+ <string name="decrypt">פענח</string>
+ <string name="conferences">ועידות</string>
+ <string name="search">חפש</string>
+ <string name="create_contact">צור איש קשר</string>
+ <string name="join_conference">הצטרף לועידה</string>
+ <string name="delete_contact">מחק איש קשר</string>
+ <string name="view_contact_details">צפה בפרטי איש קשר</string>
+ <string name="create">צור</string>
+ <string name="contact_already_exists">איש קשר כבר קיים</string>
+ <string name="join">הצטרף</string>
+ <string name="conference_address">כתובת ועידה</string>
+ <string name="conference_address_example">room@conference.example.com</string>
+ <string name="save_as_bookmark">שמור בתור סימנייה</string>
+ <string name="delete_bookmark">מחק סימנייה</string>
+ <string name="bookmark_already_exists">סימנייה זו כבר קיימת</string>
+ <string name="you">אני</string>
+ <string name="action_edit_subject">ערוך נושא ועידה</string>
+ <string name="conference_not_found">ועידה לא נמצאה</string>
+ <string name="leave">עזוב</string>
+ <string name="contact_added_you">איש קשר הוסיף אותך אל רשימת קשר</string>
+ <string name="add_back">הוסף בחזרה</string>
+ <string name="contact_has_read_up_to_this_point">%s קרא עד לנקודה זו</string>
+ <string name="touch_to_choose_picture">לחץ על אווטאר כדי לבחור תמונה מתוך גלריה</string>
+ <string name="publish_avatar_explanation">לתשומת לבך: כל מי אשר רשום לעדכוני נוכחות שלך יורשה לראות את תמונה זו.</string>
+ <string name="publishing">כעת מפרסם&#8230;</string>
+ <string name="error_publish_avatar_server_reject">השרת פסל פרסום</string>
+ <string name="error_publish_avatar_converting">משהו השתבש במהלך המרת תמונה</string>
+ <string name="error_saving_avatar">לא היה מסוגל לשמור אווטאר אל כונן</string>
+ <string name="or_long_press_for_default">(או לחיצה ארוכה כדי להחזיר לשגרה)</string>
+ <string name="error_publish_avatar_no_server_support">שרתך לא תומך בפרסום של אווטארים</string>
+ <string name="private_message">בפרטי</string>
+ <string name="private_message_to">בפרטי אל %s</string>
+ <string name="send_private_message_to">שלח הודעה פרטית אל %s</string>
+ <string name="pref_ui_options">אפשרויות ממשק משתמש</string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-nl/arrays.xml b/src/main/res/values-nl/arrays.xml
new file mode 100644
index 00000000..9ced79f4
--- /dev/null
+++ b/src/main/res/values-nl/arrays.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mobiel</item>
+ <item>Telefoon</item>
+ <item>Tablet</item>
+ <item>Conversaties</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>nooit</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml
new file mode 100644
index 00000000..7b3faca9
--- /dev/null
+++ b/src/main/res/values-nl/strings.xml
@@ -0,0 +1,233 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversaties</string>
+ <string name="action_settings">Instellingen</string>
+ <string name="action_add">Nieuwe conversatie</string>
+ <string name="action_accounts">Beheer account</string>
+ <string name="action_end_conversation">Beëindig conversatie</string>
+ <string name="action_contact_details">Contact details</string>
+ <string name="action_muc_details">Gesprek details</string>
+ <string name="action_secure">Beveiligde conversatie</string>
+ <string name="action_add_account">Voeg account toe</string>
+ <string name="action_edit_contact">Verander naam</string>
+ <string name="action_add_phone_book">Voeg aan telefoonboek toe</string>
+ <string name="action_delete_contact">Verwijder uit lijst</string>
+ <string name="title_activity_manage_accounts">Beheer Accounts</string>
+ <string name="title_activity_settings">Instellingen</string>
+ <string name="title_activity_conference_details">Groepsconversatie Details</string>
+ <string name="title_activity_contact_details">Contact Details</string>
+ <string name="title_activity_conversations">Conversaties</string>
+ <string name="title_activity_sharewith">Delen met Conversatie</string>
+ <string name="just_now">net</string>
+ <string name="minute_ago">1 min geleden</string>
+ <string name="minutes_ago">%d min geleden</string>
+ <string name="unread_conversations">ongelezen Conversaties</string>
+ <string name="sending">versturen&#8230;</string>
+ <string name="encrypted_message">Bericht aan het ontsleutelen. Een moment geduld a.u.b.&#8230;</string>
+ <string name="nick_in_use">Naam is al in gebruik</string>
+ <string name="admin">Beheerder</string>
+ <string name="owner">Eigenaar</string>
+ <string name="moderator">Moderator</string>
+ <string name="participant">Deelnemer</string>
+ <string name="visitor">Bezoeker</string>
+ <string name="remove_contact_text">Wilt u %s uit uw lijst verwijderen? De conversatie met deze account zal niet worden verwijderd.</string>
+ <string name="register_account">Registreer nieuwe account op server</string>
+ <string name="share_with">Deel met</string>
+ <string name="start_conversation">Start Conversatie</string>
+ <string name="contacts">Contacten</string>
+ <string name="cancel">Annuleer</string>
+ <string name="add">Voeg toe</string>
+ <string name="edit">Bewerk</string>
+ <string name="delete">Verwijder</string>
+ <string name="save">Sla op</string>
+ <string name="ok">OK</string>
+ <string name="crash_report_title">Conversaties is gecrashed</string>
+ <string name="crash_report_message">Door het versturen van crash rapportages helpt u mee met de ontwikkeling van Conversaties.\n<b>Waarschuwing:</b> Deze app zal uw XMPP account gebruiken om de crash rapportages te versturen naar de ontwikkelaars.</string>
+ <string name="send_now">Nu versturen</string>
+ <string name="send_never">Niet opnieuw vragen</string>
+ <string name="problem_connecting_to_account">Account verbinden mislukt</string>
+ <string name="problem_connecting_to_accounts">Verbinden met meerdere accounts mislukt</string>
+ <string name="touch_to_fix">Raak hier aan om accounts te beheren</string>
+ <string name="attach_file">Voeg bestand bij</string>
+ <string name="not_in_roster">Het contact is geen onderdeel van uw lijst. Wilt u het toevoegen?</string>
+ <string name="add_contact">Voeg contact toe</string>
+ <string name="send_failed">afleveren mislukt</string>
+ <string name="send_rejected">geweigerd</string>
+ <string name="receiving_image">Bezig met ontvangen van afbeelding. Een moment geduld a.u.b.&#8230;</string>
+ <string name="preparing_image">Bezig met voorbereiden van het versturen van afbeelding</string>
+ <string name="action_clear_history">Wis geschiedenis</string>
+ <string name="clear_conversation_history">Wis conversatie geschiedenis</string>
+ <string name="clear_histor_msg">Wilt U alle berichten in deze Conversatie verwijderen?\n\n<b>Waarschuwing:</b> Dit zal geen invloed hebben op de berichten opgeslagen op andere apparaten of servers.</string>
+ <string name="delete_messages">Verwijder berichten</string>
+ <string name="also_end_conversation">Beëindig deze conversatie na afloop</string>
+ <string name="choose_presence">Kies aanwezigheid om te tonen aan contact</string>
+ <string name="send_plain_text_message">Verstuur eenvoudig tekst bericht</string>
+ <string name="send_otr_message">Verstuur OTR versleuteld bericht</string>
+ <string name="send_pgp_message">Verstuur OpenPGP versleuteld bericht</string>
+ <string name="your_nick_has_been_changed">Uw naam is veranderd</string>
+ <string name="download_image">Download Afbeelding</string>
+ <string name="image_offered_for_download"><i>Afbeelding aangeboden voor downloaden</i></string>
+ <string name="send_unencrypted">Verstuur onversleuteld</string>
+ <string name="decryption_failed">Ontsleutelen mislukt. Misschien hebt U niet de juiste private sleutel.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversaties gebruikt een derde partij app genaamd <b>OpenKeychain</b> om berichten te versleutelen en ontsleutelen, en om publieke sleutels te beheren.\n\nOpenKeychain is beschikbaar onder de GPLv3 en beschikbaar op F-Droid en Google Play.\n\n<small>(Herstart Conversaties na installatie.)</small></string>
+ <string name="restart">Herstart</string>
+ <string name="install">Installeer</string>
+ <string name="offering">offering&#8230;</string>
+ <string name="waiting">wachten&#8230;</string>
+ <string name="no_pgp_key">Geen OpenPGP sleutel gevonden</string>
+ <string name="contact_has_no_pgp_key">Conversaties kan Uw berichten niet versleutelen omdat uw contact geen publieke sleutel heeft ingesteld.\n\n<small>Vraag uw contact om OpenPGP te configureren.</small></string>
+ <string name="no_pgp_keys">Geen OpenPGP sleutels gevonden</string>
+ <string name="contacts_have_no_pgp_keys">Conversaties kan uw berichten niet versleutelen omdat uw contacten geen publieke sleutel hebben ingesteld.\n\n<small>Vraag uw contacten om OpenPGP te configureren.</small></string>
+ <string name="encrypted_message_received"><i>Versleuteld bericht ontvangen. Raak aan om te bekijken en te ontsleutelen.</i></string>
+ <string name="encrypted_image_received"><i>Versleutelde afbeelding ontvangen. Raak aan om te bekijken en te ontsleutelen.</i></string>
+ <string name="image_file"><i>Afbeelding ontvangen. Raak aan om te bekijken.</i></string>
+ <string name="pref_xmpp_resource">XMPP resource</string>
+ <string name="pref_xmpp_resource_summary">De naam waarmee deze client zich identificeert</string>
+ <string name="pref_accept_files">Accepteer bestanden</string>
+ <string name="pref_accept_files_summary">Accepteer automatisch bestanden kleiner dan&#8230;</string>
+ <string name="pref_notification_settings">Notificatie Instellingen</string>
+ <string name="pref_notifications">Notificaties</string>
+ <string name="pref_notifications_summary">Notificatie als een nieuw bericht arriveert</string>
+ <string name="pref_vibrate">Trillen</string>
+ <string name="pref_vibrate_summary">Tril ook wanneer een nieuw bericht arriveert</string>
+ <string name="pref_sound">Geluid</string>
+ <string name="pref_sound_summary">Speel ringtone af bij notificatie</string>
+ <string name="pref_conference_notifications">Groepsconversatie notificaties</string>
+ <string name="pref_conference_notifications_summary">Toon altijd notificaties als er nieuwe berichten arriveren in groepsconversaties in plaats van alleen bij highlighting</string>
+ <string name="pref_notification_grace_period">Notificatie uitstel periode</string>
+ <string name="pref_notification_grace_period_summary">Zet notificaties voor korte tijd uit als er een carbon copy wordt ontvangen</string>
+ <string name="pref_advanced_options">Geadvanceerde Opties</string>
+ <string name="pref_never_send_crash">Verstuur nooit crash rapportages</string>
+ <string name="pref_never_send_crash_summary">Door crash rapportages te versturen helpt U mee aan de ontwikkeling van Conversaties</string>
+ <string name="pref_confirm_messages">Bevestig Berichten</string>
+ <string name="pref_confirm_messages_summary">Laat uw contacten weten waneer U berichten hebt ontvangen en gelezen</string>
+ <string name="openpgp_error">OpenKeychain rapporteerde een fout</string>
+ <string name="error_decrypting_file">I/O Fout tijdens ontsleutelen bestand</string>
+ <string name="accept">Accepteer</string>
+ <string name="error">Er is een fout opgetreden</string>
+ <string name="pref_grant_presence_updates">Verleen toestemming voor aanwezigheid updates</string>
+ <string name="pref_grant_presence_updates_summary">Vantevoren toestemming verlenen en vragen aan contacten die U hebt aangemaakt</string>
+ <string name="subscriptions">Abonnementen</string>
+ <string name="your_account">Uw account</string>
+ <string name="keys">Sleutels</string>
+ <string name="send_presence_updates">Verstuur aanwezigheid updates</string>
+ <string name="receive_presence_updates">Ontvang aanwezigheid updates</string>
+ <string name="ask_for_presence_updates">Vraag naar aanwezigheid updates</string>
+ <string name="attach_choose_picture">Kies afbeelding</string>
+ <string name="attach_take_picture">Neem foto</string>
+ <string name="preemptively_grant">Vantevoren toestemming verlenen voor abonneren</string>
+ <string name="error_not_an_image_file">Het bestand dat U gekozen hebt is geen afbeelding</string>
+ <string name="error_compressing_image">Fout tijdens converteren van afbeelding</string>
+ <string name="error_file_not_found">Bestand niet gevonden</string>
+ <string name="error_io_exception">Generieke I/O fout. Misschien is er geen opslagruimte meer beschikbaar?</string>
+ <string name="error_security_exception_during_image_copy">De app die U gebruikte om de afbeelding te selecteren heeft niet voldoende toegang geleverd om het bestand te lezen.\n\n<small>Gebruik een andere app om een afbeelding te kiezen</small></string>
+ <string name="account_status_unknown">Onbekend</string>
+ <string name="account_status_disabled">Tijdelijk uitgezet</string>
+ <string name="account_status_online">Online</string>
+ <string name="account_status_connecting">Verbinden\u2026</string>
+ <string name="account_status_offline">Offline</string>
+ <string name="account_status_unauthorized">Niet gemachtigd</string>
+ <string name="account_status_not_found">Server niet gevonden</string>
+ <string name="account_status_no_internet">Geen verbinding</string>
+ <string name="account_status_regis_fail">Registratie mislukt</string>
+ <string name="account_status_regis_conflict">Gebruikersnaam bezet</string>
+ <string name="account_status_regis_success">Registratie compleet</string>
+ <string name="account_status_regis_not_sup">Server ondersteunt geen registratie</string>
+ <string name="encryption_choice_none">Onversleuteld</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Bewerk account</string>
+ <string name="mgmt_account_delete">Verwijder</string>
+ <string name="mgmt_account_disable">Tijdelijk uitzetten</string>
+ <string name="mgmt_account_enable">Aanzetten</string>
+ <string name="mgmt_account_are_you_sure">Weet U het zeker?</string>
+ <string name="mgmt_account_delete_confirm_text">Als U uw account verwijderd wordt Uw volledige conversatie geschiedenis gewist</string>
+ <string name="attach_record_voice">Neem stem op</string>
+ <string name="account_settings_jabber_id">Jabber ID:</string>
+ <string name="account_settings_password">Wachtwoord:</string>
+ <string name="account_settings_example_jabber_id">gebruikersnaam@voorbeeld.nl</string>
+ <string name="account_settings_confirm_password">Bevestig wachtwoord:</string>
+ <string name="password">Wachtwoord</string>
+ <string name="confirm_password">Bevestig wachtwoord</string>
+ <string name="passwords_do_not_match">Wachtwoorden komen niet overeen</string>
+ <string name="invalid_jid">Dit is geen geldig Jabber ID</string>
+ <string name="error_out_of_memory">Geen geheugen beschikbaar. Afbeelding is te groot</string>
+ <string name="add_phone_book_text">Wilt U %s toevoegen aan de contactenlijst op uw telefoon?</string>
+ <string name="contact_status_online">online</string>
+ <string name="contact_status_free_to_chat">beschikbaar</string>
+ <string name="contact_status_away">weg</string>
+ <string name="contact_status_extended_away">langdurig weg</string>
+ <string name="contact_status_do_not_disturb">niet storen</string>
+ <string name="contact_status_offline">offline</string>
+ <string name="muc_details_conference">groepsconversatie</string>
+ <string name="muc_details_other_members">Andere Leden</string>
+ <string name="server_info_carbon_messages">Carbon Berichten</string>
+ <string name="server_info_stream_management">Stream Management</string>
+ <string name="missing_public_keys">Ontbrekende publieke sleutel aankondigingen</string>
+ <string name="last_seen_now">zonet voor het laatst gezien</string>
+ <string name="last_seen_min">1 minuut geleden voor het laatst gezien</string>
+ <string name="last_seen_mins">%d minuten geleden voor het laatst gezien</string>
+ <string name="last_seen_hour">1 uur geleden voor het laatst gezien</string>
+ <string name="last_seen_hours">%d uur geleden voor het laatst gezien</string>
+ <string name="last_seen_day">1 dag geleden voor het laatst gezien</string>
+ <string name="last_seen_days">%d dagen geleden voor het laatst gezien</string>
+ <string name="never_seen">nog nooit gezien</string>
+ <string name="install_openkeychain">Versleuteld bericht. Installeer OpenKeychain om te ontsleutelen.</string>
+ <string name="unknown_otr_fingerprint">Onbekende OTR vingerafdruk</string>
+ <string name="openpgp_messages_found">OpenPGP encrypted messages found</string>
+ <string name="reception_failed">Ontvangen mislukt</string>
+ <string name="join_conference">Aan groepsconversatie deelnemen</string>
+ <string name="invite_contact">Contact uitnodigen</string>
+ <string name="your_fingerprint">Uw vingerafdruk</string>
+ <string name="delete_bookmark">Bladwijzer verwijderen</string>
+ <string name="join">Deelnemen</string>
+ <string name="otr_fingerprint">OTR vingerafdruk</string>
+ <string name="you">U</string>
+ <string name="conference_not_found">Groepsconversatie niet gevonden</string>
+ <string name="search">Zoeken</string>
+ <string name="contact_already_exists">Het contact bestaat al</string>
+ <string name="title_activity_start_conversation">Start Groepsconversatie</string>
+ <string name="title_activity_choose_contact">Kies contact</string>
+ <string name="contact_added_you">Contact added you to contact list</string>
+ <string name="view_contact_details">Contactdetails bekijken</string>
+ <string name="conferences">Groepsconversaties</string>
+ <string name="verify">Controleren</string>
+ <string name="create_contact">Contact Aanmaken</string>
+ <string name="remove_bookmark_text">Wilt u %s als bladwijzer verwijderen? De groepsconversatie die verbonden is met deze bladwijzer zal niet verwijderd worden.</string>
+ <string name="action_edit_subject">Onderwerp van groepsconversatie veranderen</string>
+ <string name="delete_contact">Contact Verwijderen</string>
+ <string name="create">Aanmaken</string>
+ <string name="leave">Verlaten</string>
+ <string name="conference_address">Groepsconversatie adres</string>
+ <string name="save_as_bookmark">Bladwijzer toevoegen</string>
+ <string name="conference_address_example">kamer@groepsconversatie.voorbeeld.nl</string>
+ <string name="add_back">Terug toevoegen</string>
+ <string name="bookmark_already_exists">Deze bladwijzer bestaat al</string>
+ <string name="decrypt">Ontsleutelen</string>
+ <string name="contact_has_read_up_to_this_point">%s heeft tot hier gelezen</string>
+ <string name="next">Volgende</string>
+ <string name="publish_avatar_explanation">N.B.: Iedereen die uw aanwezigheid kan zien kan deze afbeelding zien.</string>
+ <string name="server_info_unavailable">niet beschikbaar</string>
+ <string name="mgmt_account_publish_pgp">Publiceer publieke OpenPGP sleutel</string>
+ <string name="additional_information">Extra informatie</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+ <string name="skip">Overslaan</string>
+ <string name="connect">Verbinden</string>
+ <string name="account_already_exists">Dit account bestaat al</string>
+ <string name="private_message_to">naar %s</string>
+ <string name="send_private_message_to">Verstuur privé bericht aan %s</string>
+ <string name="touch_to_choose_picture">Klik op avatar om een afbeelding te selecteren uit de gallerij</string>
+ <string name="mgmt_account_publish_avatar">Publiceer avatar</string>
+ <string name="error_publish_avatar_server_reject">De server weigerde uw publicatie</string>
+ <string name="error_publish_avatar_converting">Er ging iets mis bij het converteren van uw afbeelding</string>
+ <string name="error_publish_avatar_no_server_support">Uw server ondersteunt de publicatie van avatars niet</string>
+ <string name="publishing">Publiceren&#8230;</string>
+ <string name="error_saving_avatar">Kon de avatar niet opslaan</string>
+ <string name="server_info_session_established">Huidige sessie opgezet</string>
+ <string name="or_long_press_for_default">(Of houdt lang ingedrukt om de oorspronkelijke terug te zetten)</string>
+ <string name="server_info_available">beschikbaar</string>
+ <string name="pref_ui_options">UI Opties</string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-ru/arrays.xml b/src/main/res/values-ru/arrays.xml
new file mode 100644
index 00000000..d01d4eb9
--- /dev/null
+++ b/src/main/res/values-ru/arrays.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Мобильный</item>
+ <item>Телефон</item>
+ <item>Планшет</item>
+ <item>Conversations</item>
+ <item>Андроид</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>никогда</item>
+ <item>256 Кб</item>
+ <item>512 Кб</item>
+ <item>1 Мб</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml
new file mode 100644
index 00000000..2aa26b0b
--- /dev/null
+++ b/src/main/res/values-ru/strings.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Настройки</string>
+ <string name="action_add">Новая беседа</string>
+ <string name="action_accounts">Управление аккаунтами</string>
+ <string name="action_end_conversation">Закончить текущую беседу</string>
+ <string name="action_contact_details">Сведения о контакте</string>
+ <string name="action_muc_details">Сведения о конференции</string>
+ <string name="action_secure">Защищенная беседа</string>
+ <string name="action_add_account">Добавить аккаунт</string>
+ <string name="action_edit_contact">Редактировать контакт</string>
+ <string name="action_add_phone_book">Добавить в телефонную книгу</string>
+ <string name="action_delete_contact">Удалить из списка</string>
+ <string name="title_activity_manage_accounts">Управление Аккаунтами</string>
+ <string name="title_activity_settings">Настройки</string>
+ <string name="title_activity_conference_details">Сведения о Конференции</string>
+ <string name="title_activity_contact_details">Сведения о Контакте</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">Поделиться</string>
+ <string name="title_activity_start_conversation">Начать беседу</string>
+ <string name="title_activity_choose_contact">Выберите собеседника</string>
+ <string name="just_now">только что</string>
+ <string name="minute_ago">1 минуту назад</string>
+ <string name="minutes_ago">%d мин. назад</string>
+ <string name="unread_conversations">непрочитанных сообщений</string>
+ <string name="sending">отправка&#8230;</string>
+ <string name="encrypted_message">Расшифровка сообщения. Пожалуйста, подождите&#8230;</string>
+ <string name="nick_in_use">Имя уже используется</string>
+ <string name="admin">Администратор</string>
+ <string name="owner">Владелец</string>
+ <string name="moderator">Модератор</string>
+ <string name="participant">Участник</string>
+ <string name="visitor">Посетитель</string>
+ <string name="remove_contact_text">Вы хотите удалить %s из своего списка? Беседы, связанные с этим аккаунтом будут сохранены.</string>
+ <string name="remove_bookmark_text">Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой будут сохранены</string>
+ <string name="register_account">Создать новый аккаунт на сервере</string>
+ <string name="share_with">Поделиться с</string>
+ <string name="start_conversation">Начать беседу</string>
+ <string name="invite_contact">Пригласить собеседника</string>
+ <string name="contacts">Контакты</string>
+ <string name="cancel">Отмена</string>
+ <string name="add">Добавить</string>
+ <string name="edit">Редактировать</string>
+ <string name="delete">Удалить</string>
+ <string name="save">Сохранить</string>
+ <string name="ok">ОК</string>
+ <string name="crash_report_title">Conversations был неожиданно остановлен</string>
+ <string name="crash_report_message">Отправляя отчеты об ошибках, вы помогаете исправить и улучшить программу, поддерживая дальнейшее развитие программы\n<b>Предупреждение:</b>Отчет об ошибке будет отправлен разработчику, используя ваш аккаунт XMPP.</string>
+ <string name="send_now">Отправить сейчас</string>
+ <string name="send_never">Больше не спрашивать</string>
+ <string name="problem_connecting_to_account">Не удается подключиться к аккаунту</string>
+ <string name="problem_connecting_to_accounts">Не удается подключиться к аккаунтам</string>
+ <string name="touch_to_fix">Нажмите здесь, чтобы настроить свои аккаунты</string>
+ <string name="attach_file">Прикрепить файл</string>
+ <string name="not_in_roster">Контакт не находится в вашем списке. Хотите добавить его?</string>
+ <string name="add_contact">Добавить контакт</string>
+ <string name="send_failed">доставка не удалась</string>
+ <string name="send_rejected">отклонено</string>
+ <string name="receiving_image">Получение изображения. Пожалуйста подождите&#8230;</string>
+ <string name="preparing_image">Подготовка изображения к передаче</string>
+ <string name="action_clear_history">Очистить историю</string>
+ <string name="clear_conversation_history">Очистить историю</string>
+ <string name="clear_histor_msg">Вы хотите удалить все сообщения в этой беседе?\n\n<b>Предупреждение:</b> Данная операция не повлияет на сообщения, хранящиеся на других устройствах.</string>
+ <string name="delete_messages">Удалить сообщения</string>
+ <string name="also_end_conversation">Завершить беседу</string>
+ <string name="choose_presence">Укажите статус для контакта</string>
+ <string name="send_plain_text_message">Отправить незашифрованное текстовое сообщение</string>
+ <string name="send_otr_message">Отправить OTR защифрованное сообщение</string>
+ <string name="send_pgp_message">Отправить OpenPGP защифрованное сообщение</string>
+ <string name="your_nick_has_been_changed">Ваш псевдоним был изменен</string>
+ <string name="download_image">Загрузить изображение</string>
+ <string name="image_offered_for_download"><i>Изображение предложено для загрузки</i></string>
+ <string name="send_unencrypted">Отправить в незашифрованном виде</string>
+ <string name="decryption_failed">Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа.</string>
+ <string name="openkeychain_required">Установите OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations использует стороннее приложение под названием <b>OpenKeychain</b> для шифрования и расшифрования сообщений и управления открытыми ключами.\nПрограмма OpenKeychain распространяется под лицензией GPLv3 и доступна для загрузки через F-Droid или Google Play.\n\n<small>(Потребуется перезапуск Conversations после установки.)</small></string>
+ <string name="restart">Перезапуск</string>
+ <string name="install">Установка</string>
+ <string name="offering">предложение&#8230;</string>
+ <string name="waiting">ожидание&#8230;</string>
+ <string name="no_pgp_key">Нет OpenPGP ключа</string>
+ <string name="contact_has_no_pgp_key">Conversations не может зашифровать сообщение, потому что удаленный пользователь не анонсирует свой открытый ключ.\n\n<small>Пожалуйста, попросите удаленного пользователя тоже установить OpenPGP.</small></string>
+ <string name="no_pgp_keys">Нет OpenPGP ключей</string>
+ <string name="contacts_have_no_pgp_keys">Conversations не может зашифровать сообщения, потому что удаленные пользователи не анонсируют свои открытые ключи.\n\n<small>Пожалуйста, попросите удаленных пользователей тоже установить OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Зашифрованное сообщение получено. Нажмите здесь, чтобы расшифровать и посмотреть сообщение.</i></string>
+ <string name="encrypted_image_received"><i>Зашифрованное изображение получено. Нажмите здесь, чтобы расшифровать и посмотреть изображение.</i></string>
+ <string name="image_file"><i>Изображение получено. Нажмите здесь, чтобы посмотреть.</i></string>
+ <string name="pref_general">Общие</string>
+ <string name="pref_xmpp_resource">Название ресурса</string>
+ <string name="pref_xmpp_resource_summary">Имя которым Conversations идентифицирует себя</string>
+ <string name="pref_accept_files">Принимать файлы</string>
+ <string name="pref_accept_files_summary">Автоматический прием файлов&#8230;</string>
+ <string name="pref_notification_settings">Настройки Уведомлений</string>
+ <string name="pref_notifications">Уведомление</string>
+ <string name="pref_notifications_summary">Использовать звуковое уведомление когда приходят новые сообщения</string>
+ <string name="pref_vibrate">Вибрация</string>
+ <string name="pref_vibrate_summary">Использовать вибрацию когда приходят новые сообщения</string>
+ <string name="pref_sound">Звуковой сигнал</string>
+ <string name="pref_sound_summary">Выберите звуковой сигнал для сообщений</string>
+ <string name="pref_conference_notifications">Уведомления конференции</string>
+ <string name="pref_conference_notifications_summary">Всегда сообщать при получении нового сообщения в конференции</string>
+ <string name="pref_notification_grace_period">Отсрочка уведомлений</string>
+ <string name="pref_notification_grace_period_summary">Не использовать уведомления, если вы прочитали сообщение на другом устройстве</string>
+ <string name="pref_advanced_options">Дополнительные параметры</string>
+ <string name="pref_never_send_crash">Отчеты об ошибках</string>
+ <string name="pref_never_send_crash_summary">Отправляя отчеты об ошибках, вы помогаете исправить и улучшить Conversations, поддерживая дальнейшее развитие программы</string>
+ <string name="pref_confirm_messages">Отчеты о получении</string>
+ <string name="pref_confirm_messages_summary">Разрешить уведомлять отправителя, когда вы получили и прочитали сообщение</string>
+ <string name="pref_ui_options">Параметры интерфейса</string>
+ <string name="openpgp_error">Возникла ошибка в OpenKeychain</string>
+ <string name="error_decrypting_file">Ошибка расшифровки файла</string>
+ <string name="accept">Принять</string>
+ <string name="error">Произошла ошибка</string>
+ <string name="pref_grant_presence_updates">Предоставлять обновления</string>
+ <string name="pref_grant_presence_updates_summary">Разрешить и запрашивать статус присутствия для созданных вами контактов</string>
+ <string name="subscriptions">Подписки</string>
+ <string name="your_account">Ваш аккаунт</string>
+ <string name="keys">Ключи</string>
+ <string name="send_presence_updates">Анонсировать статус присутствия</string>
+ <string name="receive_presence_updates">Получать обновления статусов присутствия</string>
+ <string name="ask_for_presence_updates">Запрашивать обновления статусов присутствия</string>
+ <string name="attach_choose_picture">Выберите изображение</string>
+ <string name="attach_take_picture">Снимите изображение</string>
+ <string name="preemptively_grant">Удовлетворять запросы на подписки</string>
+ <string name="error_not_an_image_file">Выбранный файл не является изображением</string>
+ <string name="error_compressing_image">Ошибка при преобразовании изображения</string>
+ <string name="error_file_not_found">Файл не найден</string>
+ <string name="error_io_exception">Общая ошибка ввода/вывода. Возможно, на устройстве недостаточно свободного места?</string>
+ <string name="error_security_exception_during_image_copy">Приложение, которое было использовано для выбора изображения не имеет достаточных прав для чтения файла.\n\n<small>Используйте другой файловый менеджер, чтобы выбрать изображение</small></string>
+ <string name="account_status_unknown">Неизвестен</string>
+ <string name="account_status_disabled">Временно отключен</string>
+ <string name="account_status_online">В сети</string>
+ <string name="account_status_offline">Не в сети</string>
+ <string name="account_status_connecting">Соединение\u2026</string>
+ <string name="account_status_unauthorized">Неавторизован</string>
+ <string name="account_status_not_found">Сервер не найден</string>
+ <string name="account_status_no_internet">Нет подключения к сети</string>
+ <string name="account_status_regis_fail">Регистрация не удалась</string>
+ <string name="account_status_regis_conflict">Имя пользователя уже используется</string>
+ <string name="account_status_regis_success">Регистрация завершена</string>
+ <string name="account_status_regis_not_sup">Сервер не поддерживает регистрацию</string>
+ <string name="encryption_choice_none">Без шифрования</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Редактировать аккаунт</string>
+ <string name="mgmt_account_delete">Удалить</string>
+ <string name="mgmt_account_disable">Отключить</string>
+ <string name="mgmt_account_publish_avatar">Разместить аватар</string>
+ <string name="mgmt_account_publish_pgp">Анонсировать OpenPGP ключ</string>
+ <string name="mgmt_account_enable">Включить</string>
+ <string name="mgmt_account_are_you_sure">Вы уверены?</string>
+ <string name="mgmt_account_delete_confirm_text">Если вы удалите свой аккаунт, вся ваша история будет потеряна</string>
+ <string name="attach_record_voice">Запись голоса</string>
+ <string name="account_settings_jabber_id">JID (Джаббер ID)</string>
+ <string name="account_settings_password">Пароль</string>
+ <string name="account_settings_example_jabber_id">username@example.com</string>
+ <string name="account_settings_confirm_password">Подтвердите пароль</string>
+ <string name="password">Пароль</string>
+ <string name="confirm_password">Подтвердите пароль</string>
+ <string name="passwords_do_not_match">Пароли не совпадают</string>
+ <string name="invalid_jid">Недопустимый JID (Джаббер ID)</string>
+ <string name="error_out_of_memory">Недостаточно памяти. Изображение слишком большое</string>
+ <string name="add_phone_book_text">Вы хотите добавить %s в свою телефонную книгу?</string>
+ <string name="contact_status_online">в сети</string>
+ <string name="contact_status_free_to_chat">свободен для общения</string>
+ <string name="contact_status_away">скоро буду</string>
+ <string name="contact_status_extended_away">буду не скоро</string>
+ <string name="contact_status_do_not_disturb">не беспокоить</string>
+ <string name="contact_status_offline">не в сети</string>
+ <string name="muc_details_conference">Конференция</string>
+ <string name="muc_details_other_members">Другие участники</string>
+ <string name="server_info_carbon_messages">Дублирование сообщений</string>
+ <string name="server_info_stream_management">Управление потоками</string>
+ <string name="server_info_pep">XEP-0163: PEP (Аватары)</string>
+ <string name="server_info_available">доступен</string>
+ <string name="server_info_unavailable">недоступен</string>
+ <string name="missing_public_keys">Отсутствие анонсирования открытых ключей</string>
+ <string name="last_seen_now">Присутствие: только что</string>
+ <string name="last_seen_min">Присутствие: 1 минуту назад</string>
+ <string name="last_seen_mins">Присутствие: %d мин. назад</string>
+ <string name="last_seen_hour">Присутствие: 1 час назад</string>
+ <string name="last_seen_hours">Присутствие: %d час. назад</string>
+ <string name="last_seen_day">Присутствие: 1 день назад</string>
+ <string name="last_seen_days">Присутствие: %d дн. назад</string>
+ <string name="never_seen">Никогда</string>
+ <string name="install_openkeychain">Зашифрованное сообщение. Пожалуйста, установите OpenKeychain для дешифрования.</string>
+ <string name="unknown_otr_fingerprint">Неизвестная контрольная сумма криптографического протокола OTR</string>
+ <string name="openpgp_messages_found">Найдены OpenPGP зашифрованые сообщения</string>
+ <string name="reception_failed">Прием не удался</string>
+ <string name="your_fingerprint">Контрольная сумма</string>
+ <string name="otr_fingerprint">OTR контрольная сумма</string>
+ <string name="verify">Подтвердить</string>
+ <string name="decrypt">Дешифровать</string>
+ <string name="conferences">Конференции</string>
+ <string name="search">Поиск</string>
+ <string name="create_contact">Создать контакт</string>
+ <string name="join_conference">Присоединиться к конференции</string>
+ <string name="delete_contact">Удалить Контакт</string>
+ <string name="view_contact_details">Посмотреть данные контакта</string>
+ <string name="create">Создать</string>
+ <string name="contact_already_exists">Контакт уже существует</string>
+ <string name="join">Присоединиться</string>
+ <string name="conference_address">Адрес конференции</string>
+ <string name="conference_address_example">room@conference.example.com</string>
+ <string name="save_as_bookmark">Сохранить закладку</string>
+ <string name="delete_bookmark">Удалить закладку</string>
+ <string name="bookmark_already_exists">Такая закладка уже существует</string>
+ <string name="you">Вы</string>
+ <string name="action_edit_subject">Редактировать тему конференции</string>
+ <string name="conference_not_found">Конференция не найдена</string>
+ <string name="leave">Покинуть</string>
+ <string name="contact_added_you">Собеседник добавил вас в список контактов</string>
+ <string name="add_back">Добавить в ответ</string>
+ <string name="contact_has_read_up_to_this_point">%s прочит. сообщ. до этого момента</string>
+ <string name="publish">Опубликовать</string>
+ <string name="touch_to_choose_picture">Нажмите на аватар, чтобы выбрать новую фотографию из галереи</string>
+ <string name="publish_avatar_explanation">Пожалуйста, обратите внимание, что этот аватар смогут увидеть все ваши подписчики</string>
+ <string name="publishing">Установка&#8230;</string>
+ <string name="error_publish_avatar_server_reject">Сервер отклонил размещение аватара</string>
+ <string name="error_publish_avatar_converting">В процессе преобразования фотографии возникла ошибка</string>
+ <string name="error_saving_avatar">Не удалось сохранить аватар</string>
+ <string name="or_long_press_for_default">(Или долгое прикосновение, чтобы вернуть значения по умолчанию)</string>
+ <string name="error_publish_avatar_no_server_support">Ваш сервер не поддерживает публикацию аватаров</string>
+ <string name="private_message">Отправить личное сообщение для %s</string>
+ <string name="private_message_to">отправить %s</string>
+ <string name="send_private_message_to">Отправить личное сообщение для %s</string>
+ <string name="connect">Подключиться</string>
+ <string name="account_already_exists">Эта учетная запись уже существует</string>
+ <string name="next">Далее</string>
+ <string name="server_info_session_established">Текущий сеанс установлен</string>
+ <string name="additional_information">Дополнительная информация</string>
+ <string name="skip">Пропустить</string>
+ <string name="disable_notifications">Отключить уведомления</string>
+ <string name="disable_notifications_for_this_conversation">Отключить уведомления для текущей беседы</string>
+ <string name="notifications_disabled">Уведомления отключены</string>
+ <string name="enable">Включить</string>
+ <string name="conference_requires_password">Конференция требует авторизации</string>
+ <string name="enter_password">Введите пароль</string>
+ <string name="missing_presence_updates">Обновления присутствия недоступны</string>
+ <string name="request_presence_updates">Пожалуйста, прежде запросите обновления присутствия у вашего собеседника.\n\n<small>Эта информация будет использоваться для определения того, каким клиентом(ами) пользуетя ваш собеседник.</small></string>
+ <string name="request_now">Запросить сейчас</string>
+ <string name="delete_fingerprint">Удалить Контрольную Сумму</string>
+ <string name="sure_delete_fingerprint">Вы уверены, что хотите удалить данную контрольную сумму?</string>
+ <string name="ignore">Отменить</string>
+ <string name="without_mutual_presence_updates"><b>Внимание:</b> Если обновления присутствия не включены на обеих сторонах, это может привести к возникновению неожиданных проблемам.\n\n<small>Уточните сведения о контакте, проверив настройки обновлений присутствия.</small></string>
+ <string name="pref_encryption_settings">Настройки шифрования</string>
+ <string name="pref_force_encryption">Обязательное сквозное шифрование</string>
+ <string name="pref_force_encryption_summary">Всегда отправлять сообщения зашифрованными (за исключением конференций)</string>
+ <string name="pref_dont_save_encrypted">Не сохранять зашифрованные сообщения</string>
+ <string name="pref_dont_save_encrypted_summary">Внимание: Это может привести к потере сообщений</string>
+ <string name="pref_expert_options">Расширенные настройки</string>
+ <string name="pref_expert_options_summary">Пожалуйста, будьте осторожны с данными настройками</string>
+ <string name="pref_use_larger_font">Увеличить размер шрифта</string>
+ <string name="pref_use_larger_font_summary">Установите больший размер шрифта по всей программе</string>
+ <string name="pref_use_send_button_to_indicate_status">Использовать кнопку-индикатор</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Раскрасить кнопку отправить, указывая текущий статус собеседника</string>
+
+</resources>
diff --git a/src/main/res/values-sv/arrays.xml b/src/main/res/values-sv/arrays.xml
new file mode 100644
index 00000000..890e2915
--- /dev/null
+++ b/src/main/res/values-sv/arrays.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mobile</item>
+ <item>Phone</item>
+ <item>Tablet</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>aldrig</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml
new file mode 100644
index 00000000..a3ed9112
--- /dev/null
+++ b/src/main/res/values-sv/strings.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Inställningar</string>
+ <string name="action_add">Ny konversation</string>
+ <string name="action_accounts">Kontoinställningar</string>
+ <string name="action_end_conversation">Avsluta denna konversation</string>
+ <string name="action_contact_details">Kontaktdetaljer</string>
+ <string name="action_muc_details">Konferensdetaljer</string>
+ <string name="action_secure">Skyddad konversation</string>
+ <string name="action_add_account">Lägg till konto</string>
+ <string name="action_edit_contact">Ändra namn</string>
+ <string name="action_add_phone_book">Lägg till i telefonbok</string>
+ <string name="action_delete_contact">Ta bort kontakt</string>
+ <string name="title_activity_manage_accounts">Hantera konton</string>
+ <string name="title_activity_settings">Inställningar</string>
+ <string name="title_activity_conference_details">Konferensdetaljer</string>
+ <string name="title_activity_contact_details">Kontaktdetaljer</string>
+ <string name="title_activity_sharewith">Dela med konversation</string>
+ <string name="title_activity_start_conversation">Starta konversation</string>
+ <string name="title_activity_choose_contact">Välj kontakt</string>
+ <string name="just_now">just nu</string>
+ <string name="minute_ago">1 min sedan</string>
+ <string name="minutes_ago">%d min sedan</string>
+ <string name="unread_conversations">olästa konversationer</string>
+ <string name="sending">skickar&#8230;</string>
+ <string name="encrypted_message">Avkrypterar meddelande. Vänta&#8230;</string>
+ <string name="nick_in_use">Nick används redan</string>
+ <string name="admin">Admin</string>
+ <string name="owner">Ägare</string>
+ <string name="moderator">Moderator</string>
+ <string name="participant">Deltagare</string>
+ <string name="visitor">Besökare</string>
+ <string name="remove_contact_text">Vill du ta bort %s från din kontaktlista? Konversationer associerade med denna kontakt kommer inte tas bort.</string>
+ <string name="remove_bookmark_text">Vill du ta bort %s som bokmärke? Konversationer associerade med detta bokmärke kommer inte tas bort.</string>
+ <string name="register_account">Registrera nytt konto på servern</string>
+ <string name="share_with">Dela med</string>
+ <string name="start_conversation">Starta konversation</string>
+ <string name="invite_contact">Bjud in kontakt</string>
+ <string name="contacts">Kontakter</string>
+ <string name="cancel">Avbryt</string>
+ <string name="add">Lägg till</string>
+ <string name="edit">Ändra</string>
+ <string name="delete">Ta bort</string>
+ <string name="save">Spara</string>
+ <string name="ok">Ok</string>
+ <string name="crash_report_title">Conversations har kraschat</string>
+ <string name="crash_report_message">Genom att skicka in stack traces hjälper du utvecklarna av Conversations\n<b>Varning:</b> Detta använder ditt XMPP konto för att skicka informationen till utvecklarna.</string>
+ <string name="send_now">Skicka nu</string>
+ <string name="send_never">Fråga aldrig igen</string>
+ <string name="problem_connecting_to_account">Kan inte ansluta till konto</string>
+ <string name="problem_connecting_to_accounts">Kan inte ansluta till flera konton</string>
+ <string name="touch_to_fix">Tryck här för att hantera dina konton</string>
+ <string name="attach_file">Bifoga fil</string>
+ <string name="not_in_roster">Kontakten är inte i din kontaktlista. Vill du lägga till den?</string>
+ <string name="add_contact">Lägg till kontakt</string>
+ <string name="send_failed">sändning misslyckades</string>
+ <string name="send_rejected">avvisad</string>
+ <string name="receiving_image">Tar emot bildfil. Vänta&#8230;</string>
+ <string name="preparing_image">Förbereder bild för sändning</string>
+ <string name="action_clear_history">Rensa historik</string>
+ <string name="clear_conversation_history">Rensa konversationshistorik</string>
+ <string name="clear_histor_msg">Vill du ta bort alla meddelanden i denna konversation?\n\n<b>Varning:</b> Detta kommer inte påverka meddelanden lagrade på andra enheter eller servrar.</string>
+ <string name="delete_messages">Ta bort meddelanden</string>
+ <string name="also_end_conversation">Avsluta denna konversation efter</string>
+ <string name="send_plain_text_message">Skicka meddelande i klartext</string>
+ <string name="send_otr_message">Skicka OTR-krypterat meddelande</string>
+ <string name="send_pgp_message">Skicka OpenPGP-krypterat meddelande</string>
+ <string name="your_nick_has_been_changed">Ditt nick har ändrats</string>
+ <string name="download_image">Ladda ner bild</string>
+ <string name="image_offered_for_download"><i>Bildfil erbjuds för nedladdning</i></string>
+ <string name="send_unencrypted">Skicka okrypterat</string>
+ <string name="decryption_failed">Avkryptering gick fel. Du kanske inte har rätt privat nyckel.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations använder en tredjeparts applikation som heter <b>OpenKeychain</b> för att kryptera och avkryptera meddelanden och hantera dina publika nycklar.\n\nOpenKeychain är licensierat under GPLv3 och tillgängligt på F-Droid och Google Play.\n\n<small>(Starta om Conversations efter.)</small></string>
+ <string name="restart">Starta om</string>
+ <string name="install">Installera</string>
+ <string name="offering">erbjuder&#8230;</string>
+ <string name="waiting">väntar&#8230;</string>
+ <string name="no_pgp_key">Ingen OpenPGP-nyckel funnen</string>
+ <string name="contact_has_no_pgp_key">Conversations kan inte avkryptera ditt meddelande eftersom din kontakt inte annonserar sin publika nyckel.\n\n<small>Be din kontakt att sätta upp OpenPGP.</small></string>
+ <string name="no_pgp_keys">Inga OpenPGP-nycklar funna</string>
+ <string name="contacts_have_no_pgp_keys">Conversations kan inte avkryptera ditt meddelande eftersom din kontakt inte annonserar sin publika nyckel.\n\n<small>Be din kontakt att sätta upp OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Krypterat meddelande mottaget. Tryck för att se och avkryptera.</i></string>
+ <string name="encrypted_image_received"><i>Krypterad bild mottagen. Tryck för att se och avkryptera.</i></string>
+ <string name="image_file"><i>Bild mottagen. Tryck för att se</i></string>
+ <string name="pref_xmpp_resource">XMPP resurs</string>
+ <string name="pref_xmpp_resource_summary">Namnet som klienten identifierar sig med</string>
+ <string name="pref_accept_files">Acceptera filer</string>
+ <string name="pref_accept_files_summary">Acceptera automatistk filer som är mindre än&#8230;</string>
+ <string name="pref_notification_settings">Notifieringsinställningar</string>
+ <string name="pref_notifications">Notifieringar</string>
+ <string name="pref_notifications_summary">Notifiera när meddelande tagits emot</string>
+ <string name="pref_vibrate">Vibrera</string>
+ <string name="pref_vibrate_summary">Vibrera när meddelande tagits emot</string>
+ <string name="pref_sound">Ljud</string>
+ <string name="pref_sound_summary">Spela ljud med notifiering</string>
+ <string name="pref_conference_notifications">Konferensnotifieringar</string>
+ <string name="pref_conference_notifications_summary">Notifiera alltid när nytt konferensmeddelande tagits emot istället för endast vid highlight</string>
+ <string name="pref_notification_grace_period">Notifieringsfrist</string>
+ <string name="pref_advanced_options">Avancerade inställningar</string>
+ <string name="pref_never_send_crash">Skicka aldrig krasch-rapporter</string>
+ <string name="pref_never_send_crash_summary">Genom att skicka in stack traces hjälper du utvecklarna av Conversations</string>
+ <string name="pref_confirm_messages">Bekräfta meddelanden</string>
+ <string name="pref_confirm_messages_summary">Låter dina kontakter veta när du har tagit emot och läst ett meddelande</string>
+ <string name="openpgp_error">OpenKeychain rapporterade ett fel</string>
+ <string name="error_decrypting_file">I/O-fel vid avkryptering av fil</string>
+ <string name="accept">Acceptera</string>
+ <string name="error">Ett fel har inträffat</string>
+ <string name="pref_grant_presence_updates">Tillåt tillänglighetsuppdateringar</string>
+ <string name="pref_grant_presence_updates_summary">Tillåt i förväg och be om tillgänglighetsuppdateringar för kontakter du skapat</string>
+ <string name="subscriptions">Abonnemang</string>
+ <string name="your_account">Ditt konto</string>
+ <string name="keys">Nycklar</string>
+ <string name="send_presence_updates">Skicka tillgänglighetsuppdatering</string>
+ <string name="receive_presence_updates">Ta emot tillgänglighetsuppdateringar</string>
+ <string name="ask_for_presence_updates">Be om tillgänglighetsuppdateringar</string>
+ <string name="attach_choose_picture">Välj bild</string>
+ <string name="attach_take_picture">Ta ny bild</string>
+ <string name="preemptively_grant">Tillåt abonnemangsbegäran i förväg</string>
+ <string name="error_not_an_image_file">Filen du valt är inte en bild</string>
+ <string name="error_compressing_image">Fel vid konvertering av bildfilen</string>
+ <string name="error_file_not_found">Filen hittas ej</string>
+ <string name="error_io_exception">Generellt I/O-fel. Du kanske fick slut på plats?</string>
+ <string name="error_security_exception_during_image_copy">Applikationen du använde för att välja bilden gav inte tillräckliga rättigheter för att läsa filen.\n\n<small>Använd en annan filhanterare för att välja bild</small></string>
+ <string name="account_status_unknown">Okänd</string>
+ <string name="account_status_online">Online</string>
+ <string name="account_status_connecting">Ansluter\u2026</string>
+ <string name="account_status_offline">Offline</string>
+ <string name="account_status_unauthorized">Otillåten</string>
+ <string name="account_status_not_found">Server ej funnen</string>
+ <string name="account_status_no_internet">Ingen anslutning</string>
+ <string name="account_status_regis_fail">Registreringsfel</string>
+ <string name="account_status_regis_conflict">Användarnamn används redan</string>
+ <string name="account_status_regis_success">Registrering klar</string>
+ <string name="account_status_regis_not_sup">Servern stödjer inte registrering</string>
+ <string name="encryption_choice_none">Klartext</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Ändra konto</string>
+ <string name="mgmt_account_delete">Ta bort</string>
+ <string name="mgmt_account_enable">Aktivera</string>
+ <string name="mgmt_account_are_you_sure">Är du säker?</string>
+ <string name="mgmt_account_delete_confirm_text">Om du tar bort kontot kommer all konversationshistorik att försvinna</string>
+ <string name="attach_record_voice">Spela in röst</string>
+ <string name="account_settings_jabber_id">Jabber ID</string>
+ <string name="account_settings_password">Lösenord</string>
+ <string name="account_settings_example_jabber_id">användarnamn@exempel.se</string>
+ <string name="account_settings_confirm_password">Bekräfta lösenord</string>
+ <string name="password">Lösenord</string>
+ <string name="confirm_password">Bekräfta lösenord</string>
+ <string name="passwords_do_not_match">Lösenorden är inte lika</string>
+ <string name="invalid_jid">Detta är inte ett korrekt Jabber ID</string>
+ <string name="error_out_of_memory">Slut på minne. Bilden är för stor</string>
+ <string name="add_phone_book_text">Vill du lägga till %s i din telefons kontaktlista?</string>
+ <string name="contact_status_online">online</string>
+ <string name="contact_status_free_to_chat">tillgänglig</string>
+ <string name="contact_status_away">borta</string>
+ <string name="contact_status_extended_away">borta (förlängt)</string>
+ <string name="contact_status_do_not_disturb">stör ej</string>
+ <string name="contact_status_offline">offline</string>
+ <string name="muc_details_conference">Konferens</string>
+ <string name="muc_details_other_members">Andra medlemmar</string>
+ <string name="server_info_carbon_messages">Carbon Messages</string>
+ <string name="server_info_stream_management">Stream Management</string>
+ <string name="missing_public_keys">Annonsering om publik nyckel saknas</string>
+ <string name="last_seen_now">senast sedd just nu</string>
+ <string name="last_seen_min">senast sedd 1 minut sedan</string>
+ <string name="last_seen_mins">senast sedd %d minuter sedan</string>
+ <string name="last_seen_hour">senast sedd 1 timme sedan</string>
+ <string name="last_seen_hours">senast sedd %d timmar sedan</string>
+ <string name="last_seen_day">senast sedd 1 dag sedan</string>
+ <string name="last_seen_days">senast sedd %d dagar sedan</string>
+ <string name="never_seen">aldrig sedd</string>
+ <string name="install_openkeychain">Krypterat meddelande. Installera OpenKeychain för att avkryptera.</string>
+ <string name="unknown_otr_fingerprint">Okänt OTR-fingeravtryck</string>
+ <string name="openpgp_messages_found">OpenPGP-krypterat meddelande funnet</string>
+ <string name="reception_failed">Mottagning misslyckades</string>
+ <string name="your_fingerprint">Ditt fingeravtryck</string>
+ <string name="otr_fingerprint">OTR-fingeravtryck</string>
+ <string name="verify">Verifiera</string>
+ <string name="decrypt">Avkryptera</string>
+ <string name="conferences">Konferenser</string>
+ <string name="search">Sök</string>
+ <string name="create_contact">Skapa kontakt</string>
+ <string name="join_conference">Gå med i konferens</string>
+ <string name="delete_contact">Ta bort kontakt</string>
+ <string name="view_contact_details">Se kontaktdetaljer</string>
+ <string name="create">Skapa</string>
+ <string name="contact_already_exists">Kontakten finns redan</string>
+ <string name="join">Gå med</string>
+ <string name="conference_address">Konferensadress</string>
+ <string name="conference_address_example">rum@conference.exempel.se</string>
+ <string name="save_as_bookmark">Spara som bokmärke</string>
+ <string name="delete_bookmark">Ta bort bokmärke</string>
+ <string name="bookmark_already_exists">Detta bokmärke finns redan</string>
+ <string name="you">Du</string>
+ <string name="action_edit_subject">Ändra konferensämne</string>
+ <string name="conference_not_found">Konferens hittades inte</string>
+ <string name="leave">Lämna</string>
+ <string name="contact_added_you">Kontakten lade till dig i sin kontaktlista</string>
+ <string name="add_back">Addera tillbaks</string>
+ <string name="contact_has_read_up_to_this_point">%s har läst fram hit</string>
+ <string name="next">Nästa</string>
+ <string name="server_info_unavailable">otillgänglig</string>
+ <string name="mgmt_account_publish_pgp">Publisera OpenPGP publik nyckel</string>
+ <string name="additional_information">Ytterligare information</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatarbilder)</string>
+ <string name="skip">skippa</string>
+ <string name="connect">Anslut</string>
+ <string name="account_already_exists">Detta konto finns redan</string>
+ <string name="private_message_to">till %s</string>
+ <string name="send_private_message_to">Skicka privat meddelande till %s</string>
+ <string name="touch_to_choose_picture">Tryck på avatarbild för att välja en bild från bildgalleriet</string>
+ <string name="mgmt_account_publish_avatar">Publisera avatarbild</string>
+ <string name="error_publish_avatar_server_reject">Servern kunde inte publisera</string>
+ <string name="error_publish_avatar_converting">Något gick fel vid konvertering av din bild</string>
+ <string name="error_publish_avatar_no_server_support">Din server stödjer inte publisering av avatarbilder</string>
+ <string name="publishing">Publiserar&#8230;</string>
+ <string name="error_saving_avatar">Kunde inte spara avatarbild till disk</string>
+ <string name="server_info_session_established">Nuvarande session upprättad</string>
+ <string name="or_long_press_for_default">(Eller tryck länge för att få tillbaks förvald)</string>
+ <string name="server_info_available">tillgänglig</string>
+ <string name="pref_general">Generellt</string>
+ <string name="publish">Publicera</string>
+ <string name="private_message">privat meddelande</string>
+ <string name="pref_ui_options">UI inställningar</string>
+ <string name="enable">Aktivera</string>
+ <string name="without_mutual_presence_updates"><b>Varning:</b> Skicka detta utan gemensamma tillgänglighetsuppdateringar kan ge oväntade problem.\n\n<small>Gå till kontaktdetaljer för att verifiera dina tillgänglighetsuppdateringar.</small></string>
+ <string name="disable_notifications">Inaktivera notifieringar</string>
+ <string name="request_presence_updates">Begär tillgänglighetsuppdateringar från din kontakt först.\n\n<small>Detta används för att se vilken klient/klienter din kontakt använder.</small></string>
+ <string name="conference_requires_password">Konferensen kräver lösenord</string>
+ <string name="pref_dont_save_encrypted">Spara in krypterade meddelanden</string>
+ <string name="pref_encryption_settings">Krypteringsinställningar</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Färglägg skickaknappen för att indikera kontaktens status</string>
+ <string name="missing_presence_updates">Saknar tillgänglighetsuppdateringar från kontakt</string>
+ <string name="pref_expert_options">Expertinställningar</string>
+ <string name="pref_force_encryption_summary">Sänd alltid krypterade meddelanden (utom för konferenser)</string>
+ <string name="pref_expert_options_summary">Var försiktig med dem</string>
+ <string name="disable_notifications_for_this_conversation">Inaktivera notifieringar för denna konversation</string>
+ <string name="pref_use_send_button_to_indicate_status">Skickaknappen indikerar status</string>
+ <string name="enter_password">Fyll i lösenord</string>
+ <string name="notifications_disabled">Notifieringar är inaktiverade</string>
+ <string name="pref_force_encryption">Tvinga kryptering</string>
+ <string name="sure_delete_fingerprint">Är du säker på att du vill ta bort detta fingeravtryck?</string>
+ <string name="ignore">Ignorera</string>
+ <string name="pref_use_larger_font_summary">Använd större teckenstorlek för hela applikationen</string>
+ <string name="pref_use_larger_font">Öka teckenstorlek</string>
+ <string name="pref_dont_save_encrypted_summary">Varning: Detta kan leda till att meddelanden förloras</string>
+ <string name="delete_fingerprint">Ta bort fingeravtryck</string>
+ <string name="request_now">Begär nu</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="publish_avatar_explanation">Notera: Alla som kan se dina tillgänglighetsuppdateringar kommer se denna bild.</string>
+ <string name="choose_presence">Välj tillgänglighet till kontakt</string>
+ <string name="pref_notification_grace_period_summary">Inaktivera notifieringar en kort stund efter att en carbon copy tagits emot</string>
+ <string name="account_status_disabled">Tillfälligt inaktiverad</string>
+ <string name="mgmt_account_disable">Inaktivera tillfälligt</string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-zh-rCN/arrays.xml b/src/main/res/values-zh-rCN/arrays.xml
new file mode 100644
index 00000000..1a243079
--- /dev/null
+++ b/src/main/res/values-zh-rCN/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>手机</item>
+ <item>电话</item>
+ <item>平板电脑</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>永不</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 分钟</item>
+ <item>1 小时</item>
+ <item>2 小时</item>
+ <item>8 小时</item>
+ <item>直至另行取消</item>
+ </string-array>
+
+ <integer-array name="mute_options_durations">
+ <item>1800</item>
+ <item>3600</item>
+ <item>7200</item>
+ <item>28800</item>
+ <item>-1</item>
+ </integer-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..a7898425
--- /dev/null
+++ b/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">设置</string>
+ <string name="action_add">新会话</string>
+ <string name="action_accounts">管理账户</string>
+ <string name="action_end_conversation">结束会话</string>
+ <string name="action_contact_details">联系人详情</string>
+ <string name="action_muc_details">讨论组详情</string>
+ <string name="action_secure">安全对话</string>
+ <string name="action_add_account">添加账号</string>
+ <string name="action_edit_contact">编辑姓名</string>
+ <string name="action_add_phone_book">添加到手机通讯录</string>
+ <string name="action_delete_contact">从列表中删除</string>
+ <string name="title_activity_manage_accounts">管理账户</string>
+ <string name="title_activity_settings">设置</string>
+ <string name="title_activity_conference_details">讨论组详情</string>
+ <string name="title_activity_contact_details">联系人详情</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">分享会话</string>
+ <string name="title_activity_start_conversation">开始会话</string>
+ <string name="title_activity_choose_contact">选择联系人</string>
+ <string name="just_now">刚刚</string>
+ <string name="minute_ago">1 分钟前</string>
+ <string name="minutes_ago">%d分钟前</string>
+ <string name="unread_conversations">未读会话</string>
+ <string name="sending">正在发送&#8230;</string>
+ <string name="encrypted_message">解密信息中. 请稍候&#8230;</string>
+ <string name="nick_in_use">该名称已存在</string>
+ <string name="admin">管理员</string>
+ <string name="owner">所有者</string>
+ <string name="moderator">版主</string>
+ <string name="participant">参与者</string>
+ <string name="visitor">访客</string>
+ <string name="remove_contact_text">将 %s从列表中移除? 与该联系人的会话消息不会清除.</string>
+ <string name="remove_bookmark_text">从书签中移除 %s?相关会话消息不会被清除 .</string>
+ <string name="register_account">在服务器上注册新账户</string>
+ <string name="share_with">分享</string>
+ <string name="start_conversation">开始会话</string>
+ <string name="invite_contact">邀请联系人</string>
+ <string name="contacts">联系人</string>
+ <string name="cancel">取消</string>
+ <string name="add">添加</string>
+ <string name="edit">编辑</string>
+ <string name="delete">删除</string>
+ <string name="save">保存</string>
+ <string name="ok">完成</string>
+ <string name="crash_report_title">Conversations停止运行</string>
+ <string name="crash_report_message">发送堆栈跟踪到正在开发Conversations的人员\n<b>警告:</b> 该操作将用您的 XMPP账户发送堆栈跟踪到开发人员.</string>
+ <string name="send_now">现在发送</string>
+ <string name="send_never">不再询问</string>
+ <string name="problem_connecting_to_account">无法连接至账户</string>
+ <string name="problem_connecting_to_accounts">无法连接至多个账户</string>
+ <string name="touch_to_fix">点击此处管理账户</string>
+ <string name="attach_file">附件</string>
+ <string name="not_in_roster">该联系人不在您的列表.需要加为联系人吗 ?</string>
+ <string name="add_contact">添加联系人</string>
+ <string name="send_failed">传递失败</string>
+ <string name="send_rejected">拒绝</string>
+ <string name="receiving_image">接收图片文件中. 请稍候&#8230;</string>
+ <string name="preparing_image">准备传输图像</string>
+ <string name="action_clear_history">清除历史记录</string>
+ <string name="clear_conversation_history">清除会话记录</string>
+ <string name="clear_histor_msg">删除该会话中所有信息?\n\n<b>注:</b> 该操作不会影响其他设备或服务器保存的信息.</string>
+ <string name="delete_messages">删除消息</string>
+ <string name="also_end_conversation">之后结束该会话</string>
+ <string name="choose_presence">添加在线用户至联系人</string>
+ <string name="send_plain_text_message">发送纯文本信息</string>
+ <string name="send_otr_message">发送 OTR 加密信息</string>
+ <string name="send_pgp_message">发送 OpenPGP 加密信息</string>
+ <string name="your_nick_has_been_changed">用户名修改成功</string>
+ <string name="download_image">下载图片</string>
+ <string name="image_offered_for_download"><i>供下载的图像文件</i></string>
+ <string name="send_unencrypted">不加密发送</string>
+ <string name="decryption_failed">解密失败,可能是私钥不正确.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">会话运用了第三方app,名为 <b>OpenKeychain</b> 用来加密、解码信息以及管理您的公钥.\n\nOpenKeychain 遵循 GPLv3 并且在 F-Droid和Google Play上可操作.\n\n<small>(之后请重启conversations.)</small></string>
+ <string name="restart">重启</string>
+ <string name="install">安装</string>
+ <string name="offering">输入&#8230;</string>
+ <string name="waiting">等待&#8230;</string>
+ <string name="no_pgp_key">未发现OpenPGP 密码</string>
+ <string name="contact_has_no_pgp_key">会话加密信息失败,因为联系人未告知他/她的公钥.\n\n<small>请通知联系人设置 OpenPGP.</small></string>
+ <string name="no_pgp_keys">未找到 OpenPGP 密码</string>
+ <string name="contacts_have_no_pgp_keys">因您的联系人未公布公钥,Conversations未能成功加密您的信息.\n\n<small>请通知联系人设置OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>加密信息已接收.点击进行解密和查看.</i></string>
+ <string name="encrypted_image_received"><i>加密图像已接收.点击进行解密和查看.</i></string>
+ <string name="image_file"><i>图片已成功接收,点击查看</i></string>
+ <string name="pref_general">常规</string>
+ <string name="pref_xmpp_resource">XMPP 资源</string>
+ <string name="pref_xmpp_resource_summary">客户端标识名称</string>
+ <string name="pref_accept_files">接收文件</string>
+ <string name="pref_accept_files_summary">自动接收小于 &#8230; 的文件</string>
+ <string name="pref_notification_settings">通知设置</string>
+ <string name="pref_notifications">通知</string>
+ <string name="pref_notifications_summary">收到新消息时通知</string>
+ <string name="pref_vibrate">震动</string>
+ <string name="pref_vibrate_summary">收到新消息时震动</string>
+ <string name="pref_sound">声音</string>
+ <string name="pref_sound_summary">收到新消息时播放铃声</string>
+ <string name="pref_conference_notifications">讨论组通知</string>
+ <string name="pref_conference_notifications_summary">当有新的消息时总是通知而不是亮屏时才通知</string>
+ <string name="pref_notification_grace_period">通知限期</string>
+ <string name="pref_notification_grace_period_summary">接收副本短时间内关闭通知</string>
+ <string name="pref_advanced_options">高级选项</string>
+ <string name="pref_never_send_crash">总不发送故障报告</string>
+ <string name="pref_never_send_crash_summary">发送堆栈跟踪帮助Conversations开发人员</string>
+ <string name="pref_confirm_messages">确认消息</string>
+ <string name="pref_confirm_messages_summary">当你已收到消息并且已阅时通知好友</string>
+ <string name="pref_ui_options">UI选项</string>
+ <string name="openpgp_error">OpenKeychain 报告了一个错误</string>
+ <string name="error_decrypting_file">解码文件时出现I/O错误</string>
+ <string name="accept">接受</string>
+ <string name="error">产生了一个错误</string>
+ <string name="pref_grant_presence_updates">同意更新在线联系人</string>
+ <string name="pref_grant_presence_updates_summary">预先同意并请求您的联系人进行更新</string>
+ <string name="subscriptions">关注</string>
+ <string name="your_account">你的账号</string>
+ <string name="keys">Keys</string>
+ <string name="send_presence_updates">发送在线联系人更新列表</string>
+ <string name="receive_presence_updates">接收在线联系人更新列表</string>
+ <string name="ask_for_presence_updates">请求在线联系人更新列表</string>
+ <string name="attach_choose_picture">选择图片</string>
+ <string name="attach_take_picture">照相</string>
+ <string name="preemptively_grant">预先同意订阅请求</string>
+ <string name="error_not_an_image_file">您选择的文件不是图像文件</string>
+ <string name="error_compressing_image">转换图像出错</string>
+ <string name="error_file_not_found">未找到文件</string>
+ <string name="error_io_exception">常规的I/O错误.可能是存储空间不足的原因?</string>
+ <string name="error_security_exception_during_image_copy">您用来选择图片的app没有给予足够权限支持我们读取文件.\n\n<small>请使用另一文件管理器选择图片</small></string>
+ <string name="account_status_unknown">未知</string>
+ <string name="account_status_disabled">暂时不可用</string>
+ <string name="account_status_online">在线</string>
+ <string name="account_status_connecting">Connecting\u2026</string>
+ <string name="account_status_offline">离线</string>
+ <string name="account_status_unauthorized">未授权</string>
+ <string name="account_status_not_found">未找到服务器</string>
+ <string name="account_status_no_internet">未连接网络</string>
+ <string name="account_status_regis_fail">注册失败</string>
+ <string name="account_status_regis_conflict"> 用户名已存在</string>
+ <string name="account_status_regis_success">注册完成</string>
+ <string name="account_status_regis_not_sup">服务器不支持注册</string>
+ <string name="encryption_choice_none">纯文本内容</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">编辑账号</string>
+ <string name="mgmt_account_delete">删除账号</string>
+ <string name="mgmt_account_disable">暂时不可用</string>
+ <string name="mgmt_account_publish_avatar">发布头像</string>
+ <string name="mgmt_account_publish_pgp">发布 OpenPGP 公共秘钥</string>
+ <string name="mgmt_account_enable">启用账户</string>
+ <string name="mgmt_account_are_you_sure">确定?</string>
+ <string name="mgmt_account_delete_confirm_text">如果删除用户,所有会话信息将会丢失</string>
+ <string name="attach_record_voice">Record voice 录音</string>
+ <string name="account_settings_jabber_id">Jabber ID</string>
+ <string name="account_settings_password">密码</string>
+ <string name="account_settings_example_jabber_id">username@example.com</string>
+ <string name="account_settings_confirm_password">确认密码</string>
+ <string name="password">密码</string>
+ <string name="confirm_password">确认密码</string>
+ <string name="passwords_do_not_match">密码不一致</string>
+ <string name="invalid_jid">该Jabber ID 无效</string>
+ <string name="error_out_of_memory">空间不足,图片过大</string>
+ <string name="add_phone_book_text">您将添加 %s 至手机联系人列表?</string>
+ <string name="contact_status_online">在线</string>
+ <string name="contact_status_free_to_chat">免费对话</string>
+ <string name="contact_status_away">离开</string>
+ <string name="contact_status_extended_away">长时间离开</string>
+ <string name="contact_status_do_not_disturb">请勿打扰</string>
+ <string name="contact_status_offline">离线</string>
+ <string name="muc_details_conference">讨论组</string>
+ <string name="muc_details_other_members">其他成员</string>
+ <string name="server_info_carbon_messages">XEP-0280: 消息碳</string>
+ <string name="server_info_stream_management">XEP-0198: 流管理</string>
+ <string name="server_info_pep">XEP-0163: PEP (头像)</string>
+ <string name="server_info_available">有效</string>
+ <string name="server_info_unavailable">无效</string>
+ <string name="missing_public_keys">缺少公共秘钥公告</string>
+ <string name="last_seen_now">最近一次查看为刚刚</string>
+ <string name="last_seen_min"> 最近一次查看为一分钟前</string>
+ <string name="last_seen_mins">最近一次查看为 %d 分钟前</string>
+ <string name="last_seen_hour">最近一次查看为一小时前</string>
+ <string name="last_seen_hours">最近一次查看为 %d 小时前</string>
+ <string name="last_seen_day">最近一次查看为一天前</string>
+ <string name="last_seen_days">最近一次查看为 %d天前</string>
+ <string name="never_seen">未曾查看</string>
+ <string name="install_openkeychain">加密信息. 请安装OpenKeychain进行解码.</string>
+ <string name="unknown_otr_fingerprint">未知 OTR指纹</string>
+ <string name="openpgp_messages_found">OpenPGP 发现加密信息</string>
+ <string name="reception_failed">接收失败</string>
+ <string name="your_fingerprint">你的指纹</string>
+ <string name="otr_fingerprint">OTR 指纹</string>
+ <string name="verify">验证</string>
+ <string name="decrypt">解密</string>
+ <string name="conferences">讨论组</string>
+ <string name="search">查找</string>
+ <string name="create_contact">创建联系人</string>
+ <string name="join_conference">加入讨论组</string>
+ <string name="delete_contact">删除联系人</string>
+ <string name="view_contact_details">查看联系人详细信息</string>
+ <string name="create">创建</string>
+ <string name="contact_already_exists">联系人已存在</string>
+ <string name="join">加入</string>
+ <string name="conference_address">讨论组地址</string>
+ <string name="conference_address_example">room@conference.example.com</string>
+ <string name="save_as_bookmark">保存为书签</string>
+ <string name="delete_bookmark">删除书签</string>
+ <string name="bookmark_already_exists">该书签已存在</string>
+ <string name="you">你的</string>
+ <string name="action_edit_subject">编辑讨论组主题</string>
+ <string name="conference_not_found">讨论组未找到</string>
+ <string name="leave">离开</string>
+ <string name="contact_added_you">联系人已添加你到联系人列表</string>
+ <string name="add_back">反向添加</string>
+ <string name="contact_has_read_up_to_this_point">目前读到%s 处</string>
+ <string name="publish">发布</string>
+ <string name="touch_to_choose_picture">点击头像可选择头像 </string>
+ <string name="publish_avatar_explanation">请注意: 所有关注您最新动态的人将看到该图像.</string>
+ <string name="publishing">发布&#8230;</string>
+ <string name="error_publish_avatar_server_reject">服务器拒绝了您的发布请求</string>
+ <string name="error_publish_avatar_converting">转换头像出错</string>
+ <string name="error_saving_avatar">不能将头像保存至disk</string>
+ <string name="or_long_press_for_default">(或长按按钮将返回默认头像)</string>
+ <string name="error_publish_avatar_no_server_support">您的服务器不支持发布头像</string>
+ <string name="private_message">密谈</string>
+ <string name="private_message_to">至 %s</string>
+ <string name="send_private_message_to">发送私密消息到%s</string>
+ <string name="connect">Connect</string>
+ <string name="account_already_exists">该账号已存在</string>
+ <string name="next">下一步</string>
+ <string name="server_info_session_established">当前会话已建立</string>
+ <string name="additional_information">其他信息</string>
+ <string name="skip">Skip略过</string>
+ <string name="disable_notifications">关闭通知</string>
+ <string name="disable_notifications_for_this_conversation">关闭该会话消息</string>
+ <string name="notifications_disabled">通知已关闭</string>
+ <string name="enable">打开通知</string>
+ <string name="conference_requires_password">讨论组设有密码</string>
+ <string name="enter_password">输入密码</string>
+ <string name="missing_presence_updates">缺少在线联系人更新</string>
+ <string name="request_presence_updates">请先发送更新在线联系人请求.\n\n<small>这将用来判断您的联系人所用的客户端类型人.</small></string>
+ <string name="request_now">现在发送请求</string>
+ <string name="delete_fingerprint">删除指纹</string>
+ <string name="sure_delete_fingerprint">是否确定删除该指纹?</string>
+ <string name="ignore">忽略</string>
+ <string name="without_mutual_presence_updates"><b>警告:</b>在没有相互更新在线联系人的情况下发送将会出现未知问题.\n\n<small>到联系人详情确认您订阅的在线联系人.</small></string>
+ <string name="pref_encryption_settings">加密设置</string>
+ <string name="pref_force_encryption">强制要求 end-to-end 加密</string>
+ <string name="pref_force_encryption_summary"> 总是发送加密信息(讨论组信息除外)</string>
+ <string name="pref_dont_save_encrypted">不保存加密信息</string>
+ <string name="pref_dont_save_encrypted_summary">警告:此操作将会导致信息丢失</string>
+ <string name="pref_expert_options">Expert 选项</string>
+ <string name="pref_expert_options_summary">请谨慎使用</string>
+ <string name="pref_use_larger_font"> 放大字体</string>
+ <string name="pref_use_larger_font_summary">整个app界面使用更大号的字体</string>
+ <string name="pref_use_send_button_to_indicate_status">发送按钮显示状态</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">发送按钮采用其他颜色以示发送状态的区别</string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-zh-rTW/arrays.xml b/src/main/res/values-zh-rTW/arrays.xml
new file mode 100644
index 00000000..b9c261ad
--- /dev/null
+++ b/src/main/res/values-zh-rTW/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>手機</item>
+ <item>電話</item>
+ <item>平板電腦</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>永不</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 分鐘</item>
+ <item>1 小時</item>
+ <item>2 小時</item>
+ <item>8 小時</item>
+ <item>直至另行取消</item>
+ </string-array>
+
+ <integer-array name="mute_options_durations">
+ <item>1800</item>
+ <item>3600</item>
+ <item>7200</item>
+ <item>28800</item>
+ <item>-1</item>
+ </integer-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..2c3ea225
--- /dev/null
+++ b/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,263 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">設定</string>
+ <string name="action_add">新對話</string>
+ <string name="action_accounts">管理帳戶</string>
+ <string name="action_end_conversation">結束對話</string>
+ <string name="action_contact_details">聯絡人詳情</string>
+ <string name="action_secure">安全對話</string>
+ <string name="action_add_account">新增帳戶</string>
+ <string name="action_edit_contact">編輯姓名</string>
+ <string name="action_add_phone_book">新增到手機通訊錄</string>
+ <string name="action_delete_contact">從列表中刪除</string>
+ <string name="title_activity_manage_accounts">管理帳戶</string>
+ <string name="title_activity_conference_details">群組詳情</string>
+ <string name="title_activity_contact_details">聯絡人詳情</string>
+ <string name="title_activity_conversations">對話</string>
+ <string name="title_activity_sharewith">分享對話</string>
+ <string name="title_activity_start_conversation">開始對話</string>
+ <string name="title_activity_choose_contact">選擇聯絡人</string>
+ <string name="just_now">剛剛</string>
+ <string name="minute_ago">1 分鐘前</string>
+ <string name="minutes_ago">%d 分鐘前</string>
+ <string name="unread_conversations">未讀對話</string>
+ <string name="sending">正在發送&#8230;</string>
+ <string name="encrypted_message">正在解密訊息中,請稍候&#8230;</string>
+ <string name="nick_in_use">該用戶名稱已被使用</string>
+ <string name="admin">管理員</string>
+ <string name="owner">擁有人</string>
+ <string name="moderator">版主</string>
+ <string name="participant">成員</string>
+ <string name="visitor">訪客</string>
+ <string name="remove_contact_text">你確定要將 %s 從聯絡人清單中移除嗎?與該聯絡人的對話將不會被清除。</string>
+ <string name="remove_bookmark_text">你確定要將 %s 從書籤清單中移除嗎?與該聯絡人的對話將不會被清除。</string>
+ <string name="register_account">在伺服器上註冊新帳戶</string>
+ <string name="share_with">分享</string>
+ <string name="start_conversation">開始對話</string>
+ <string name="invite_contact">邀請聯絡人</string>
+ <string name="contacts">聯絡人</string>
+ <string name="cancel">取消</string>
+ <string name="add">新增</string>
+ <string name="edit">編輯</string>
+ <string name="delete">刪除</string>
+ <string name="save">儲存</string>
+ <string name="ok">好的</string>
+ <string name="crash_report_title">Conversations 停止運行</string>
+ <string name="crash_report_message">發送「堆疊追蹤」給 Conversations 的開發人員能幫助改進本程式。\n<b>警告:</b> 你的 XMPP 帳戶將被用作發送有關訊息之用。</string>
+ <string name="send_now">現在發送</string>
+ <string name="send_never">不再詢問</string>
+ <string name="problem_connecting_to_account">無法連接至帳戶</string>
+ <string name="problem_connecting_to_accounts">無法連接至多個帳戶</string>
+ <string name="touch_to_fix">點擊此處管理帳戶。</string>
+ <string name="attach_file">附件</string>
+ <string name="not_in_roster">該聯絡人不在你的聯絡人清單上,需要加為聯絡人嗎?</string>
+ <string name="add_contact">新增聯絡人</string>
+ <string name="send_failed">傳遞失敗</string>
+ <string name="send_rejected">拒絕</string>
+ <string name="receiving_image">接收圖片文件中,請稍候&#8230;</string>
+ <string name="preparing_image">準備傳輸圖片</string>
+ <string name="action_clear_history">清除歷史記錄</string>
+ <string name="clear_conversation_history">清除對話記錄</string>
+ <string name="clear_histor_msg">你確定要刪除該對話中所有訊息嗎?\n\n<b>警告:</b> 這將不會影響其他設備或伺服器儲存的訊息。</string>
+ <string name="delete_messages">刪除訊息</string>
+ <string name="also_end_conversation">之後結束這對話</string>
+ <string name="choose_presence">選擇狀態訊息</string>
+ <string name="send_plain_text_message">發送純文字訊息</string>
+ <string name="send_otr_message">發送 OTR 加密訊息</string>
+ <string name="send_pgp_message">發送 OpenPGP 加密訊息</string>
+ <string name="your_nick_has_been_changed">用戶名稱修改成功</string>
+ <string name="download_image">下載圖片</string>
+ <string name="image_offered_for_download"><i>可供下載的圖像文件</i></string>
+ <string name="send_unencrypted">不加密發送</string>
+ <string name="decryption_failed">解密失敗,可能是私鑰不正確。</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations 使用一個名為 <b>OpenKeychain</b> 的第三方程式來加密、解碼訊息以及管理您的公鑰。\n\nOpenKeychain 以 GPLv3 釋出,並可在 F-Droid 和 Google Play 上下載。\n\n<small>(之後請重新啟動 Conversations。)</small></string>
+ <string name="restart">重新啟動</string>
+ <string name="install">安裝</string>
+ <string name="offering">提供中&#8230;</string>
+ <string name="waiting">等待中&#8230;</string>
+ <string name="no_pgp_key">找不到 OpenPGP 鑰匙</string>
+ <string name="contact_has_no_pgp_key">Conversations 不能將你的訊息加密,因為聯絡人沒有公佈他/她的公鑰。\n\n<small>請通知聯絡人設定 OpenPGP。</small></string>
+ <string name="no_pgp_keys">找不到多條 OpenPGP 鑰匙</string>
+ <string name="contacts_have_no_pgp_keys">Conversations 不能將你的訊息加密,因為多位聯絡人沒有公佈他/她的公鑰。\n\n<small>請通知聯絡人設定 OpenPGP。</small></string>
+ <string name="encrypted_message_received"><i>已收到加密訊息,點擊進行解密和查看。</i></string>
+ <string name="encrypted_image_received"><i>已收到加密圖片,點擊進行解密和查看。</i></string>
+ <string name="image_file"><i>已收到圖片,點擊查看</i></string>
+ <string name="pref_general">一般</string>
+ <string name="pref_xmpp_resource">XMPP 資源</string>
+ <string name="pref_xmpp_resource_summary">客戶端標示名稱</string>
+ <string name="pref_accept_files">接收文件</string>
+ <string name="pref_accept_files_summary">自動接收小於 &#8230; 的文件</string>
+ <string name="pref_notification_settings">通知設定</string>
+ <string name="pref_notifications">通知</string>
+ <string name="pref_notifications_summary">收到新訊息時通知</string>
+ <string name="pref_vibrate">震動</string>
+ <string name="pref_vibrate_summary">收到新訊息時震動</string>
+ <string name="pref_sound">聲音</string>
+ <string name="pref_sound_summary">收到新訊息時播放鈴聲</string>
+ <string name="pref_conference_notifications">群組通知</string>
+ <string name="pref_conference_notifications_summary">當有新訊息時總是通知,而不是被標記時才通知</string>
+ <string name="pref_notification_grace_period">通知限期</string>
+ <string name="pref_notification_grace_period_summary">收到副本後,關閉通知一小段時間</string>
+ <string name="pref_advanced_options">進階選項</string>
+ <string name="pref_never_send_crash">總是不發送故障報告</string>
+ <string name="pref_never_send_crash_summary">發送「堆疊追蹤」給 Conversations 的開發人員能幫助改進本程式</string>
+ <string name="pref_confirm_messages">確認訊息</string>
+ <string name="pref_confirm_messages_summary">讓你的聯絡人知道你已收到及閱讀訊息</string>
+ <string name="pref_ui_options">介面選項</string>
+ <string name="openpgp_error">OpenKeychain 回報了一個錯誤</string>
+ <string name="error_decrypting_file">解密文件時出現 I/O 錯誤</string>
+ <string name="accept">接受</string>
+ <string name="error">發生了一個錯誤</string>
+ <string name="pref_grant_presence_updates">同意更新狀態訊息</string>
+ <string name="pref_grant_presence_updates_summary">預先更新狀態訊息並關注聯絡人的狀態訊息</string>
+ <string name="subscriptions">關注</string>
+ <string name="your_account">你的帳戶</string>
+ <string name="keys">鑰匙</string>
+ <string name="send_presence_updates">發送狀態訊息</string>
+ <string name="receive_presence_updates">接收狀態訊息</string>
+ <string name="ask_for_presence_updates">關注狀態訊息</string>
+ <string name="attach_choose_picture">選擇圖片</string>
+ <string name="attach_take_picture">拍照</string>
+ <string name="preemptively_grant">預先同意關注請求</string>
+ <string name="error_not_an_image_file">您選擇的文件不是圖片</string>
+ <string name="error_compressing_image">轉換圖片時發生錯誤</string>
+ <string name="error_file_not_found">找不到文件</string>
+ <string name="error_io_exception">一般的 I/O 錯誤。是存儲空間不足嗎?</string>
+ <string name="error_security_exception_during_image_copy">你用來選擇圖片的 app 沒有給予足夠權限我們去讀取文件。\n\n<small>請使用另一文件管理器來選擇圖片</small></string>
+ <string name="account_status_unknown">未知</string>
+ <string name="account_status_disabled">暫時停用</string>
+ <string name="account_status_online">在線</string>
+ <string name="account_status_connecting">連接中\u2026</string>
+ <string name="account_status_offline">離線</string>
+ <string name="account_status_unauthorized">未授權</string>
+ <string name="account_status_not_found">未找到伺服器</string>
+ <string name="account_status_no_internet">未連接網絡</string>
+ <string name="account_status_regis_fail">註冊失敗</string>
+ <string name="account_status_regis_conflict">該用戶名稱已被使用</string>
+ <string name="account_status_regis_success">註冊完成</string>
+ <string name="account_status_regis_not_sup">伺服器不支持註冊</string>
+ <string name="encryption_choice_none">純文字內容</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">編輯帳戶</string>
+ <string name="mgmt_account_delete">刪除帳戶</string>
+ <string name="mgmt_account_disable">暫時停用</string>
+ <string name="mgmt_account_publish_avatar">發佈頭像</string>
+ <string name="mgmt_account_publish_pgp">發布 OpenPGP 公共鑰匙</string>
+ <string name="mgmt_account_enable">啟用帳戶</string>
+ <string name="mgmt_account_are_you_sure">你確定嗎?</string>
+ <string name="mgmt_account_delete_confirm_text">如果刪除帳戶,則所有對話訊息將會被刪除</string>
+ <string name="attach_record_voice">錄音</string>
+ <string name="account_settings_jabber_id">Jabber ID</string>
+ <string name="account_settings_password">密碼</string>
+ <string name="account_settings_example_jabber_id">username@example.com</string>
+ <string name="account_settings_confirm_password">確認密碼</string>
+ <string name="password">密碼</string>
+ <string name="confirm_password">確認密碼</string>
+ <string name="passwords_do_not_match">密碼不一致</string>
+ <string name="invalid_jid">該 Jabber ID 無效</string>
+ <string name="error_out_of_memory">空間不足,圖片過大</string>
+ <string name="add_phone_book_text">你確定要新增 %s 為聯絡人嗎?</string>
+ <string name="contact_status_online">線上</string>
+ <string name="contact_status_free_to_chat">目前有空</string>
+ <string name="contact_status_away">離開</string>
+ <string name="contact_status_extended_away">長時間離開</string>
+ <string name="contact_status_do_not_disturb">請勿打擾</string>
+ <string name="contact_status_offline">離線</string>
+ <string name="muc_details_conference">群組</string>
+ <string name="muc_details_other_members">其他成員</string>
+ <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+ <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+ <string name="server_info_available">支援</string>
+ <string name="server_info_unavailable">不支援</string>
+ <string name="missing_public_keys">沒有公佈公鑰訊息。</string>
+ <string name="last_seen_now">剛剛曾在線上</string>
+ <string name="last_seen_min">一分鐘前曾在線上</string>
+ <string name="last_seen_mins">%d 分鐘前曾在線上</string>
+ <string name="last_seen_hour">一小時前曾在線上</string>
+ <string name="last_seen_hours">%d 小時前曾在線上</string>
+ <string name="last_seen_day">一天前曾在線上</string>
+ <string name="last_seen_days">%d 天前曾在線上</string>
+ <string name="never_seen">未曾上線</string>
+ <string name="install_openkeychain">加密的訊息。請安裝 OpenKeychain 以解密。</string>
+ <string name="unknown_otr_fingerprint">未知的 OTR 指紋</string>
+ <string name="openpgp_messages_found">發現以 OpenPGP 加密的訊息</string>
+ <string name="reception_failed">接收失敗</string>
+ <string name="your_fingerprint">你的指紋</string>
+ <string name="otr_fingerprint">OTR 指紋</string>
+ <string name="verify">驗證</string>
+ <string name="decrypt">解密</string>
+ <string name="conferences">群組</string>
+ <string name="search">查找</string>
+ <string name="create_contact">新增聯絡人</string>
+ <string name="join_conference">加入群組</string>
+ <string name="delete_contact">刪除聯絡人</string>
+ <string name="view_contact_details">查看聯絡人詳細訊息</string>
+ <string name="create">新增</string>
+ <string name="contact_already_exists">聯絡人已存在</string>
+ <string name="join">加入</string>
+ <string name="conference_address">群組地址</string>
+ <string name="conference_address_example">room@conference.example.com</string>
+ <string name="save_as_bookmark">儲存為書籤</string>
+ <string name="delete_bookmark">刪除書籤</string>
+ <string name="bookmark_already_exists">該書籤已存在</string>
+ <string name="you">你</string>
+ <string name="action_edit_subject">編輯群組主題</string>
+ <string name="conference_not_found">群組未找到</string>
+ <string name="leave">離開</string>
+ <string name="contact_added_you">聯絡人已新增你到聯絡人列表</string>
+ <string name="add_back">新增為聯絡人</string>
+ <string name="contact_has_read_up_to_this_point">%s 讀到此處</string>
+ <string name="publish">發佈</string>
+ <string name="touch_to_choose_picture">點擊頭像可選擇頭像</string>
+ <string name="publish_avatar_explanation">請注意: 所有關注你狀態訊息的人將看到該圖像。</string>
+ <string name="publishing">發佈中&#8230;</string>
+ <string name="error_publish_avatar_server_reject">伺服器拒絕了你的發佈請求</string>
+ <string name="error_publish_avatar_converting">發佈頭像時發生錯誤</string>
+ <string name="error_saving_avatar">將頭像儲存至硬碟時發生錯誤</string>
+ <string name="or_long_press_for_default">(或長按以回復預設頭像)</string>
+ <string name="error_publish_avatar_no_server_support">你的伺服器不支持發佈頭像</string>
+ <string name="private_message">私密聊天</string>
+ <string name="private_message_to">給 %s</string>
+ <string name="send_private_message_to">發送私密消息給 %s</string>
+ <string name="connect">連接</string>
+ <string name="account_already_exists">該帳戶已存在</string>
+ <string name="next">下一步</string>
+ <string name="server_info_session_established">已建立連接</string>
+ <string name="additional_information">其他訊息</string>
+ <string name="skip">略過</string>
+ <string name="disable_notifications">關閉通知</string>
+ <string name="disable_notifications_for_this_conversation">關閉該對話消息</string>
+ <string name="notifications_disabled">通知已關閉</string>
+ <string name="enable">打開通知</string>
+ <string name="conference_requires_password">群組設有密碼</string>
+ <string name="enter_password">輸入密碼</string>
+ <string name="missing_presence_updates">缺少聯絡人狀態訊息</string>
+ <string name="request_presence_updates">請先發送關注狀態訊息請求。\n\n<small>這將用來判斷您的聯絡人所用的客戶端類型。</small></string>
+ <string name="request_now">現在發送請求</string>
+ <string name="delete_fingerprint">刪除指紋</string>
+ <string name="sure_delete_fingerprint">你確定刪除該指紋嗎?</string>
+ <string name="ignore">忽略</string>
+ <string name="without_mutual_presence_updates"><b>警告:</b> 在沒有互相關注狀態訊息的情況下發送或會引起不能預計的問題。\n\n<small>請檢視聯絡人詳情頁面以確認你們的關注狀態。</small></string>
+ <string name="pref_encryption_settings">加密設定</string>
+ <string name="pref_force_encryption">強制要求端到端加密</string>
+ <string name="pref_force_encryption_summary">總是發送加密訊息 (群組訊息除外)</string>
+ <string name="pref_dont_save_encrypted">不儲存加密訊息</string>
+ <string name="pref_dont_save_encrypted_summary">警告: 此操作或會導致訊息丟失</string>
+ <string name="pref_expert_options">專家選項</string>
+ <string name="pref_expert_options_summary">請小心設定</string>
+ <string name="pref_use_larger_font">增加字體大小</string>
+ <string name="pref_use_larger_font_summary">讓整個 app 界面使用更大號的字體</string>
+ <string name="pref_use_send_button_to_indicate_status">用「發送」按鈕顯示狀態訊息</string>
+ <string name="pref_use_indicate_received">要求讀取收據</string>
+ <string name="pref_use_indicate_received_summary">已被讀取的訊息會以綠色勾號表示。請注意,這個功能未必每次有效。</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">將「發送」按鈕設成不同顏色,以表示不同的狀態訊息。</string>
+ <string name="pref_expert_options_other">其他</string>
+ <string name="pref_conference_name">群組名稱</string>
+ <string name="pref_conference_name_summary">使用群組的名稱而不是 JID 來識別之。 </string>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..1a4fd25d
--- /dev/null
+++ b/src/main/res/values/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="resources">
+ <item>Mobile</item>
+ <item>Phone</item>
+ <item>Tablet</item>
+ <item>Conversations</item>
+ <item>Android</item>
+ </string-array>
+ <string-array name="filesizes">
+ <item>never</item>
+ <item>256 KB</item>
+ <item>512 KB</item>
+ <item>1 MB</item>
+ </string-array>
+ <string-array name="filesizes_values">
+ <item>0</item>
+ <item>262144</item>
+ <item>524288</item>
+ <item>1048576</item>
+ </string-array>
+ <string-array name="mute_options_descriptions">
+ <item>30 minutes</item>
+ <item>one hour</item>
+ <item>2 hours</item>
+ <item>8 hours</item>
+ <item>until further notice</item>
+ </string-array>
+
+ <integer-array name="mute_options_durations">
+ <item>1800</item>
+ <item>3600</item>
+ <item>7200</item>
+ <item>28800</item>
+ <item>-1</item>
+ </integer-array>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..2354a5e8
--- /dev/null
+++ b/src/main/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <attr name="TextSizeInfo" format="dimension" />
+ <attr name="TextSizeBody" format="dimension" />
+ <attr name="TextSizeHeadline" format="dimension" />
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml
new file mode 100644
index 00000000..908b8b89
--- /dev/null
+++ b/src/main/res/values/colors.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <color name="primary" type="color">#ff259b24</color>
+ <color name="primarydark" type="color">#ff0a7e07</color>
+ <color name="primarytext" type="color">#de000000</color>
+ <color name="secondarytext" type="color">#8a000000</color>
+ <color name="ondarktext" type="color">#fffafafa</color>
+ <color name="primarybackground" type="color">#fffafafa</color>
+ <color name="secondarybackground" type="color">#ffeeeeee</color>
+ <color name="darkbackground" type="color">#ff323232</color>
+ <color name="divider">#1f000000</color>
+ <color name="red">#ffe51c23</color>
+ <color name="orange">#ffff9800</color>
+ <color name="green">#ff259b24</color>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3862bb7b
--- /dev/null
+++ b/src/main/res/values/strings.xml
@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Conversations</string>
+ <string name="action_settings">Settings</string>
+ <string name="action_add">New conversation</string>
+ <string name="action_accounts">Manage accounts</string>
+ <string name="action_end_conversation">End this conversation</string>
+ <string name="action_contact_details">Contact details</string>
+ <string name="action_muc_details">Conference details</string>
+ <string name="action_secure">Secure conversation</string>
+ <string name="action_add_account">Add account</string>
+ <string name="action_edit_contact">Edit name</string>
+ <string name="action_add_phone_book">Add to phone book</string>
+ <string name="action_delete_contact">Delete from roster</string>
+ <string name="title_activity_manage_accounts">Manage Accounts</string>
+ <string name="title_activity_settings">Settings</string>
+ <string name="title_activity_conference_details">Conference Details</string>
+ <string name="title_activity_contact_details">Contact Details</string>
+ <string name="title_activity_conversations">Conversations</string>
+ <string name="title_activity_sharewith">Share with Conversation</string>
+ <string name="title_activity_start_conversation">Start Conversation</string>
+ <string name="title_activity_choose_contact">Choose contact</string>
+ <string name="just_now">just now</string>
+ <string name="minute_ago">1 min ago</string>
+ <string name="minutes_ago">%d mins ago</string>
+ <string name="unread_conversations">unread Conversations</string>
+ <string name="sending">sending&#8230;</string>
+ <string name="encrypted_message">Decrypting message. Please wait&#8230;</string>
+ <string name="nick_in_use">Nickname is already in use</string>
+ <string name="admin">Admin</string>
+ <string name="owner">Owner</string>
+ <string name="moderator">Moderator</string>
+ <string name="participant">Participant</string>
+ <string name="visitor">Visitor</string>
+ <string name="remove_contact_text">Would you like to remove %s from your roster? The conversation associated with this contact will not be removed.</string>
+ <string name="remove_bookmark_text">Would you like to remove %s as a bookmark? The conversation associated with this bookmark will not be removed.</string>
+ <string name="register_account">Register new account on server</string>
+ <string name="share_with">Share with</string>
+ <string name="start_conversation">Start Conversation</string>
+ <string name="invite_contact">Invite Contact</string>
+ <string name="contacts">Contacts</string>
+ <string name="cancel">Cancel</string>
+ <string name="add">Add</string>
+ <string name="edit">Edit</string>
+ <string name="delete">Delete</string>
+ <string name="save">Save</string>
+ <string name="ok">OK</string>
+ <string name="crash_report_title">Conversations has crashed</string>
+ <string name="crash_report_message">By sending in stack traces you are helping the ongoing development of Conversations\n<b>Warning:</b> This will use your XMPP account to send the stack trace to the developer.</string>
+ <string name="send_now">Send now</string>
+ <string name="send_never">Never ask again</string>
+ <string name="problem_connecting_to_account">Unable to connect to account</string>
+ <string name="problem_connecting_to_accounts">Unable to connect to multiple accounts</string>
+ <string name="touch_to_fix">Touch here to manage your accounts</string>
+ <string name="attach_file">Attach file</string>
+ <string name="not_in_roster">The contact is not in your roster. Would you like to add it?</string>
+ <string name="add_contact">Add contact</string>
+ <string name="send_failed">delivery failed</string>
+ <string name="send_rejected">rejected</string>
+ <string name="receiving_image">Receiving image file. Please wait&#8230;</string>
+ <string name="preparing_image">Preparing image for transmission</string>
+ <string name="action_clear_history">Clear history</string>
+ <string name="clear_conversation_history">Clear Conversation History</string>
+ <string name="clear_histor_msg">Do you want to delete all messages within this Conversation?\n\n<b>Warning:</b> This will not influence messages stored on other devices or servers.</string>
+ <string name="delete_messages">Delete messages</string>
+ <string name="also_end_conversation">End this conversations afterwards</string>
+ <string name="choose_presence">Choose presence to contact</string>
+ <string name="send_plain_text_message">Send plain text message</string>
+ <string name="send_otr_message">Send OTR encrypted message</string>
+ <string name="send_pgp_message">Send OpenPGP encrypted message</string>
+ <string name="your_nick_has_been_changed">Your nickname has been changed</string>
+ <string name="download_image">Download Image</string>
+ <string name="image_offered_for_download"><i>Image file offered for download</i></string>
+ <string name="send_unencrypted">Send unencrypted</string>
+ <string name="decryption_failed">Decryption failed. Maybe you don’t have the proper private key.</string>
+ <string name="openkeychain_required">OpenKeychain</string>
+ <string name="openkeychain_required_long">Conversations utilizes a third party app called <b>OpenKeychain</b> to encrypt and decrypt messages and to manage your public keys.\n\nOpenKeychain is licensed under GPLv3 and available on F-Droid and Google Play.\n\n<small>(Please restart Conversations afterwards.)</small></string>
+ <string name="restart">Restart</string>
+ <string name="install">Install</string>
+ <string name="offering">offering&#8230;</string>
+ <string name="waiting">waiting&#8230;</string>
+ <string name="no_pgp_key">No OpenPGP Key found</string>
+ <string name="contact_has_no_pgp_key">Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\n<small>Please ask your contact to setup OpenPGP.</small></string>
+ <string name="no_pgp_keys">No OpenPGP Keys found</string>
+ <string name="contacts_have_no_pgp_keys">Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\n<small>Please ask your contacts to setup OpenPGP.</small></string>
+ <string name="encrypted_message_received"><i>Encrypted message received. Touch to view and decrypt.</i></string>
+ <string name="encrypted_image_received"><i>Encrypted image received. Touch to view and decrypt.</i></string>
+ <string name="image_file"><i>Image received. Touch to view</i></string>
+ <string name="pref_general">General</string>
+ <string name="pref_xmpp_resource">XMPP resource</string>
+ <string name="pref_xmpp_resource_summary">The name this client identifies itself with</string>
+ <string name="pref_accept_files">Accept files</string>
+ <string name="pref_accept_files_summary">Automatically accept files smaller than&#8230;</string>
+ <string name="pref_notification_settings">Notification Settings</string>
+ <string name="pref_notifications">Notifications</string>
+ <string name="pref_notifications_summary">Notify when a new message arrives</string>
+ <string name="pref_vibrate">Vibrate</string>
+ <string name="pref_vibrate_summary">Also vibrate when a new message arrives</string>
+ <string name="pref_sound">Sound</string>
+ <string name="pref_sound_summary">Play ringtone with notification</string>
+ <string name="pref_conference_notifications">Conference notifications</string>
+ <string name="pref_conference_notifications_summary">Always notify when a new conference message arrives instead of only when highlighted</string>
+ <string name="pref_notification_grace_period">Notification grace period</string>
+ <string name="pref_notification_grace_period_summary">Disable notifications for a short time after a carbon copy was received</string>
+ <string name="pref_advanced_options">Advanced Options</string>
+ <string name="pref_never_send_crash">Never send crash reports</string>
+ <string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string>
+ <string name="pref_confirm_messages">Confirm Messages</string>
+ <string name="pref_confirm_messages_summary">Let your contact know when you have received and read a message</string>
+ <string name="pref_ui_options">UI Options</string>
+ <string name="openpgp_error">OpenKeychain reported an error</string>
+ <string name="error_decrypting_file">I/O Error decrypting file</string>
+ <string name="accept">Accept</string>
+ <string name="error">An error has occurred</string>
+ <string name="pref_grant_presence_updates">Grant presence updates</string>
+ <string name="pref_grant_presence_updates_summary">Preemptively grant and ask for presence subscription for contacts you created</string>
+ <string name="subscriptions">Subscriptions</string>
+ <string name="your_account">Your account</string>
+ <string name="keys">Keys</string>
+ <string name="send_presence_updates">Send presence updates</string>
+ <string name="receive_presence_updates">Receive presence updates</string>
+ <string name="ask_for_presence_updates">Ask for presence updates</string>
+ <string name="attach_choose_picture">Choose picture</string>
+ <string name="attach_take_picture">Take picture</string>
+ <string name="preemptively_grant">Preemptively grant subscription request</string>
+ <string name="error_not_an_image_file">The file you selected is not an image</string>
+ <string name="error_compressing_image">Error while converting the image file</string>
+ <string name="error_file_not_found">File not found</string>
+ <string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string>
+ <string name="error_security_exception_during_image_copy">The app you used to select this image did not provide us with enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small></string>
+ <string name="account_status_unknown">Unknown</string>
+ <string name="account_status_disabled">Temporarily disabled</string>
+ <string name="account_status_online">Online</string>
+ <string name="account_status_connecting">Connecting\u2026</string>
+ <string name="account_status_offline">Offline</string>
+ <string name="account_status_unauthorized">Unauthorized</string>
+ <string name="account_status_not_found">Server not found</string>
+ <string name="account_status_no_internet">No connectivity</string>
+ <string name="account_status_regis_fail">Registration failed</string>
+ <string name="account_status_regis_conflict">Username already in use</string>
+ <string name="account_status_regis_success">Registration completed</string>
+ <string name="account_status_regis_not_sup">Server does not support registration</string>
+ <string name="encryption_choice_none">Plain text</string>
+ <string name="encryption_choice_otr">OTR</string>
+ <string name="encryption_choice_pgp">OpenPGP</string>
+ <string name="mgmt_account_edit">Edit account</string>
+ <string name="mgmt_account_delete">Delete account</string>
+ <string name="mgmt_account_disable">Temporarily disable</string>
+ <string name="mgmt_account_publish_avatar">Publish avatar</string>
+ <string name="mgmt_account_publish_pgp">Publish OpenPGP public key</string>
+ <string name="mgmt_account_enable">Enable account</string>
+ <string name="mgmt_account_are_you_sure">Are you sure?</string>
+ <string name="mgmt_account_delete_confirm_text">If you delete your account your entire conversation history will be lost</string>
+ <string name="attach_record_voice">Record voice</string>
+ <string name="account_settings_jabber_id">Jabber ID</string>
+ <string name="account_settings_password">Password</string>
+ <string name="account_settings_example_jabber_id">username@example.com</string>
+ <string name="account_settings_confirm_password">Confirm password</string>
+ <string name="password">Password</string>
+ <string name="confirm_password">Confirm password</string>
+ <string name="passwords_do_not_match">Passwords do not match</string>
+ <string name="invalid_jid">This is not a valid Jabber ID</string>
+ <string name="error_out_of_memory">Out of memory. Image is too large</string>
+ <string name="add_phone_book_text">Do you want to add %s to your phones contact list?</string>
+ <string name="contact_status_online">online</string>
+ <string name="contact_status_free_to_chat">free to chat</string>
+ <string name="contact_status_away">away</string>
+ <string name="contact_status_extended_away">extended away</string>
+ <string name="contact_status_do_not_disturb">do not disturb</string>
+ <string name="contact_status_offline">offline</string>
+ <string name="muc_details_conference">Conference</string>
+ <string name="muc_details_other_members">Other Members</string>
+ <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+ <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+ <string name="server_info_available">available</string>
+ <string name="server_info_unavailable">unavailable</string>
+ <string name="missing_public_keys">Missing public key announcements</string>
+ <string name="last_seen_now">last seen just now</string>
+ <string name="last_seen_min">last seen 1 minute ago</string>
+ <string name="last_seen_mins">last seen %d minutes ago</string>
+ <string name="last_seen_hour">last seen 1 hour ago</string>
+ <string name="last_seen_hours">last seen %d hours ago</string>
+ <string name="last_seen_day">last seen 1 day ago</string>
+ <string name="last_seen_days">last seen %d days ago</string>
+ <string name="never_seen">never seen</string>
+ <string name="install_openkeychain">Encrypted message. Please install OpenKeychain to decrypt.</string>
+ <string name="unknown_otr_fingerprint">Unknown OTR fingerprint</string>
+ <string name="openpgp_messages_found">OpenPGP encrypted messages found</string>
+ <string name="reception_failed">Reception failed</string>
+ <string name="your_fingerprint">Your fingerprint</string>
+ <string name="otr_fingerprint">OTR fingerprint</string>
+ <string name="verify">Verify</string>
+ <string name="decrypt">Decrypt</string>
+ <string name="conferences">Conferences</string>
+ <string name="search">Search</string>
+ <string name="create_contact">Create Contact</string>
+ <string name="join_conference">Join Conference</string>
+ <string name="delete_contact">Delete Contact</string>
+ <string name="view_contact_details">View contact details</string>
+ <string name="create">Create</string>
+ <string name="contact_already_exists">The contact already exists</string>
+ <string name="join">Join</string>
+ <string name="conference_address">Conference address</string>
+ <string name="conference_address_example">room@conference.example.com</string>
+ <string name="save_as_bookmark">Save as bookmark</string>
+ <string name="delete_bookmark">Delete bookmark</string>
+ <string name="bookmark_already_exists">This bookmark already exists</string>
+ <string name="you">You</string>
+ <string name="action_edit_subject">Edit conference subject</string>
+ <string name="conference_not_found">Conference not found</string>
+ <string name="leave">Leave</string>
+ <string name="contact_added_you">Contact added you to contact list</string>
+ <string name="add_back">Add back</string>
+ <string name="contact_has_read_up_to_this_point">%s has read up to this point</string>
+ <string name="publish">Publish</string>
+ <string name="touch_to_choose_picture">Touch avatar to select picture from gallery</string>
+ <string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string>
+ <string name="publishing">Publishing&#8230;</string>
+ <string name="error_publish_avatar_server_reject">The server rejected your publication</string>
+ <string name="error_publish_avatar_converting">Something went wrong while converting your picture</string>
+ <string name="error_saving_avatar">Could not save avatar to disk</string>
+ <string name="or_long_press_for_default">(Or long press to bring back default)</string>
+ <string name="error_publish_avatar_no_server_support">Your server does not support the publication of avatars</string>
+ <string name="private_message">whispered</string>
+ <string name="private_message_to">to %s</string>
+ <string name="send_private_message_to">Send private message to %s</string>
+ <string name="connect">Connect</string>
+ <string name="account_already_exists">This account does already exist</string>
+ <string name="next">Next</string>
+ <string name="server_info_session_established">Current session established</string>
+ <string name="additional_information">Additional Information</string>
+ <string name="skip">Skip</string>
+ <string name="disable_notifications">Disable notifications</string>
+ <string name="disable_notifications_for_this_conversation">Disable notifications for this conversation</string>
+ <string name="notifications_disabled">Notifications are disabled</string>
+ <string name="enable">Enable</string>
+ <string name="conference_requires_password">Conference requires password</string>
+ <string name="enter_password">Enter password</string>
+ <string name="missing_presence_updates">Missing presence updates from contact</string>
+ <string name="request_presence_updates">Please request presence updates from your contact first.\n\n<small>This will be used to determine what client(s) your contact is using.</small></string>
+ <string name="request_now">Request now</string>
+ <string name="delete_fingerprint">Delete Fingerprint</string>
+ <string name="sure_delete_fingerprint">Are you sure you would like to delete this fingerprint?</string>
+ <string name="ignore">Ignore</string>
+ <string name="without_mutual_presence_updates"><b>Warning:</b> Sending this without mutual presence updates could cause unexpected problems.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string>
+ <string name="pref_encryption_settings">Encryption settings</string>
+ <string name="pref_force_encryption">Force end-to-end encryption</string>
+ <string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string>
+ <string name="pref_dont_save_encrypted">Don’t save encrypted messages</string>
+ <string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string>
+ <string name="pref_enable_legacy_ssl">Enable legacy SSL</string>
+ <string name="pref_enable_legacy_ssl_summary">Enables SSLv3 support for legacy servers. Warning: SSLv3 is considered insecure.</string>
+ <string name="pref_expert_options">Expert options</string>
+ <string name="pref_expert_options_summary">Please be careful with these</string>
+ <string name="pref_use_larger_font">Increase font size</string>
+ <string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</string>
+ <string name="pref_use_send_button_to_indicate_status">Send button indicates status</string>
+ <string name="pref_use_indicate_received">Request message receipts</string>
+ <string name="pref_use_indicate_received_summary">Received messages will be marked with a green tick if supported</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Colorize send button to indicate contact status</string>
+ <string name="pref_expert_options_other">Other</string>
+ <string name="pref_conference_name">Conference name</string>
+ <string name="pref_conference_name_summary">Use room’s subject instead of JID to identify conferences</string>
+ <string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
+ <string name="conference_banned">You are banned from this conference</string>
+ <string name="conference_members_only">This conference is members only</string>
+ <string name="conference_kicked">You have been kicked from this conference</string>
+ <string name="using_account">using account %s</string>
+ <string name="checking_image">Checking image on HTTP host</string>
+ <string name="image_file_deleted">The image file has been deleted</string>
+ <string name="not_connected_try_again">You are not connected. Try again later</string>
+ <string name="check_image_filesize">Check image file size</string>
+
+</resources>
diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml
new file mode 100644
index 00000000..64bde770
--- /dev/null
+++ b/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="Divider">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">1.5dp</item>
+ <item name="android:background">@color/divider</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml
new file mode 100644
index 00000000..fa7973d2
--- /dev/null
+++ b/src/main/res/values/themes.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="ConversationsTheme" parent="@android:style/Theme.Holo.Light.DarkActionBar">
+ <item name="android:actionBarStyle">@style/ConversationsActionBar</item>
+ <item name="android:actionBarWidgetTheme">@style/ConversationsActionBarWidget</item>
+ <item name="android:actionBarTabStyle">@style/ConversationsActionBarTabs</item>
+ <item name="TextSizeInfo">12sp</item>
+ <item name="TextSizeBody">14sp</item>
+ <item name="TextSizeHeadline">20sp</item>
+ </style>
+
+ <style name="ConversationsTheme.LargerText" parent="ConversationsTheme">
+ <item name="TextSizeInfo">14sp</item>
+ <item name="TextSizeBody">16sp</item>
+ <item name="TextSizeHeadline">22sp</item>
+ </style>
+
+ <style name="ConversationsActionBar" parent="@android:style/Widget.Holo.Light.ActionBar.Solid.Inverse">
+ <item name="android:background">@color/primary</item>
+ <item name="android:backgroundStacked">@color/primarydark</item>
+ <item name="android:displayOptions">showHome|homeAsUp|showTitle</item>
+ <item name="android:icon">@android:color/transparent</item>
+ </style>
+
+ <style name="ConversationsActionBarWidget" parent="android:Theme.Holo.Light">
+ <item name="android:popupMenuStyle">@android:style/Widget.Holo.Light.PopupMenu</item>
+ <item name="android:dropDownListViewStyle">@android:style/Widget.Holo.Light.ListView.DropDown</item>
+ </style>
+
+ <style name="ConversationsActionBarTabs" parent="@android:style/Widget.Holo.ActionBar.TabView">
+ <item name="android:background">@drawable/actionbar_tab_indicator</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
new file mode 100644
index 00000000..06ab7560
--- /dev/null
+++ b/src/main/res/xml/preferences.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <PreferenceCategory android:title="@string/pref_general" >
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="grant_new_contacts"
+ android:summary="@string/pref_grant_presence_updates_summary"
+ android:title="@string/pref_grant_presence_updates" />
+
+ <ListPreference
+ android:defaultValue="Mobile"
+ android:entries="@array/resources"
+ android:entryValues="@array/resources"
+ android:key="resource"
+ android:summary="@string/pref_xmpp_resource_summary"
+ android:title="@string/pref_xmpp_resource" />
+ <ListPreference
+ android:defaultValue="524288"
+ android:entries="@array/filesizes"
+ android:entryValues="@array/filesizes_values"
+ android:key="auto_accept_file_size"
+ android:summary="@string/pref_accept_files_summary"
+ android:title="@string/pref_accept_files" />
+
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="confirm_messages"
+ android:summary="@string/pref_confirm_messages_summary"
+ android:title="@string/pref_confirm_messages" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_notification_settings" >
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="show_notification"
+ android:summary="@string/pref_notifications_summary"
+ android:title="@string/pref_notifications" />
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:dependency="show_notification"
+ android:key="vibrate_on_notification"
+ android:summary="@string/pref_vibrate_summary"
+ android:title="@string/pref_vibrate" />
+
+ <RingtonePreference
+ android:defaultValue="content://settings/system/notification_sound"
+ android:dependency="show_notification"
+ android:key="notification_ringtone"
+ android:ringtoneType="notification"
+ android:summary="@string/pref_sound_summary"
+ android:title="@string/pref_sound" />
+
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:dependency="show_notification"
+ android:key="always_notify_in_conference"
+ android:summary="@string/pref_conference_notifications_summary"
+ android:title="@string/pref_conference_notifications" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_ui_options" >
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="use_subject"
+ android:summary="@string/pref_conference_name_summary"
+ android:title="@string/pref_conference_name" />
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="use_larger_font"
+ android:summary="@string/pref_use_larger_font_summary"
+ android:title="@string/pref_use_larger_font" />
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="send_button_status"
+ android:summary="@string/pref_use_send_button_to_indicate_status_summary"
+ android:title="@string/pref_use_send_button_to_indicate_status" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_advanced_options" >
+ <PreferenceScreen
+ android:summary="@string/pref_expert_options_summary"
+ android:title="@string/pref_expert_options" >
+ <PreferenceCategory android:title="@string/pref_encryption_settings" >
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="force_encryption"
+ android:summary="@string/pref_force_encryption_summary"
+ android:title="@string/pref_force_encryption" />
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="dont_save_encrypted"
+ android:summary="@string/pref_dont_save_encrypted_summary"
+ android:title="@string/pref_dont_save_encrypted" />
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="enable_legacy_ssl"
+ android:summary="@string/pref_enable_legacy_ssl_summary"
+ android:title="@string/pref_enable_legacy_ssl" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_expert_options_other" >
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="indicate_received"
+ android:summary="@string/pref_use_indicate_received_summary"
+ android:title="@string/pref_use_indicate_received" />
+ </PreferenceCategory>
+ </PreferenceScreen>
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="never_send"
+ android:summary="@string/pref_never_send_crash_summary"
+ android:title="@string/pref_never_send_crash" />
+ </PreferenceCategory>
+
+</PreferenceScreen>