aboutsummaryrefslogtreecommitdiffstats
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/AndroidManifest.xml118
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java26
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/OtrEngine.java231
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpEngine.java394
-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.java512
-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.java270
-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.java525
-rw-r--r--src/main/java/eu/siacs/conversations/parser/PresenceParser.java133
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java353
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/FileBackend.java447
-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.java292
-rw-r--r--src/main/java/eu/siacs/conversations/services/EventReceiver.java24
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java333
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java1949
-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.java434
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java936
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java892
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java410
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditMessage.java39
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java218
-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.java648
-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.java139
-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.java555
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java112
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java171
-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/ExifHelper.java161
-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.java1129
-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.java902
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java168
-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/message_context.xml20
-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.xml265
-rw-r--r--src/main/res/values-de/arrays.xml31
-rw-r--r--src/main/res/values-de/strings.xml285
-rw-r--r--src/main/res/values-es/arrays.xml39
-rw-r--r--src/main/res/values-es/strings.xml285
-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.xml286
-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
315 files changed, 26030 insertions, 0 deletions
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/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
new file mode 100644
index 00000000..7dd5a799
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -0,0 +1,26 @@
+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 MINI_GRACE_PERIOD = 750;
+
+ public static final int AVATAR_SIZE = 192;
+ public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
+
+ public static final int MESSAGE_MERGE_WINDOW = 20;
+
+ public static final boolean 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..9a2b4a11
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
@@ -0,0 +1,394 @@
+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 java.net.URL;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
+
+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.net.Uri;
+
+public class PgpEngine {
+ private OpenPgpApi api;
+ private XmppConnectionService mXmppConnectionService;
+
+ public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
+ this.api = api;
+ this.mXmppConnectionService = service;
+ }
+
+ public void decrypt(final Message message,
+ final UiCallback<Message> callback) {
+ 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);
+ if (message.trusted() && message.bodyContainsDownloadable()) {
+ mXmppConnectionService.getHttpConnectionManager()
+ .createNewConnection(message);
+ }
+ callback.success(message);
+ }
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, message);
+ return;
+ }
+
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried((PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ 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:
+ URL url = message.getImageParams().url;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(
+ outputFile.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ if (url == null) {
+ message.setBody(Long.toString(outputFile
+ .getSize())
+ + '|'
+ + imageWidth
+ + '|'
+ + imageHeight);
+ } else {
+ message.setBody(url.toString() + "|"
+ + Long.toString(outputFile.getSize())
+ + '|' + imageWidth + '|' + imageHeight);
+ }
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ PgpEngine.this.mXmppConnectionService
+ .updateMessage(message);
+ inputFile.delete();
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(outputFile));
+ mXmppConnectionService.sendBroadcast(intent);
+ callback.success(message);
+ return;
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ callback.userInputRequried(
+ (PendingIntent) result
+ .getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+ message);
+ return;
+ case OpenPgpApi.RESULT_CODE_ERROR:
+ callback.error(R.string.openpgp_error, message);
+ 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) {
+ callback.error(R.string.openpgp_error, message);
+ return;
+ } catch (IOException e) {
+ callback.error(R.string.openpgp_error, message);
+ return;
+ }
+ }
+ }
+
+ 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:
+ 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..8a83c465
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -0,0 +1,512 @@
+package eu.siacs.conversations.entities;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+import eu.siacs.conversations.Config;
+import android.content.ContentValues;
+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_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 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))))
+ && !message.bodyContainsDownloadable()
+ && !this.bodyContainsDownloadable());
+ }
+
+ 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 trusted() {
+ Contact contact = this.getContact();
+ return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
+ }
+
+ public boolean bodyContainsDownloadable() {
+ 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;
+ if (pathParts.length > 0) {
+ filename = pathParts[pathParts.length - 1];
+ } else {
+ filename = pathParts[0];
+ }
+ 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 = getLegacyImageParams();
+ if (params!=null) {
+ return params;
+ }
+ params = new ImageParams();
+ if (this.downloadable != null) {
+ params.size = this.downloadable.getFileSize();
+ }
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split("\\|");
+ if (parts.length == 1) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ params.origin = parts[0];
+ try {
+ params.url = new URL(parts[0]);
+ } catch (MalformedURLException e1) {
+ params.url = null;
+ }
+ }
+ } else if (parts.length == 3) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[1]);
+ } catch (NumberFormatException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ params.height = 0;
+ }
+ } else if (parts.length == 4) {
+ params.origin = parts[0];
+ try {
+ params.url = new URL(parts[0]);
+ } catch (MalformedURLException e1) {
+ params.url = null;
+ }
+ try {
+ params.size = Long.parseLong(parts[1]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[3]);
+ } catch (NumberFormatException e) {
+ params.height = 0;
+ }
+ }
+ return params;
+ }
+
+ public ImageParams getLegacyImageParams() {
+ ImageParams params = new ImageParams();
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split(",");
+ if (parts.length == 3) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ try {
+ params.width = Integer.parseInt(parts[1]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ try {
+ params.height = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return params;
+ } else {
+ return null;
+ }
+ }
+
+ public class ImageParams {
+ public URL url;
+ public long size = 0;
+ public int width = 0;
+ public int height = 0;
+ public String origin;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
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..0810e167
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnection.java
@@ -0,0 +1,270 @@
+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 eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class HttpConnection implements Downloadable {
+
+ private HttpConnectionManager mHttpConnectionManager;
+ private XmppConnectionService mXmppConnectionService;
+
+ private URL mUrl;
+ private Message message;
+ private DownloadableFile file;
+ private int mStatus = Downloadable.STATUS_UNKNOWN;
+ private boolean acceptedAutomatically = false;
+
+ public HttpConnection(HttpConnectionManager manager) {
+ this.mHttpConnectionManager = manager;
+ this.mXmppConnectionService = manager.getXmppConnectionService();
+ }
+
+ @Override
+ public boolean start() {
+ if (mXmppConnectionService.hasInternetConnection()) {
+ if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
+ checkFileSize(true);
+ } else {
+ new Thread(new FileDownloader(true)).start();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void init(Message message) {
+ this.message = message;
+ this.message.setDownloadable(this);
+ try {
+ mUrl = new URL(message.getBody());
+ String path = mUrl.getPath();
+ if (path != null && (path.endsWith(".pgp") || path.endsWith(".gpg"))) {
+ this.message.setEncryption(Message.ENCRYPTION_PGP);
+ } else if (message.getEncryption() != Message.ENCRYPTION_OTR) {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
+ }
+ this.file = mXmppConnectionService.getFileBackend().getFile(
+ message, false);
+ String reference = mUrl.getRef();
+ if (reference != null && reference.length() == 96) {
+ this.file.setKey(CryptoHelper.hexToBytes(reference));
+ }
+
+ if (this.message.getEncryption() == Message.ENCRYPTION_OTR
+ && this.file.getKey() == null) {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
+ }
+ checkFileSize(false);
+ } catch (MalformedURLException e) {
+ this.cancel();
+ }
+ }
+
+ private void checkFileSize(boolean interactive) {
+ new Thread(new FileSizeChecker(interactive)).start();
+ }
+
+ public void cancel() {
+ mHttpConnectionManager.finishConnection(this);
+ message.setDownloadable(null);
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void finish() {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ mXmppConnectionService.sendBroadcast(intent);
+ message.setDownloadable(null);
+ mHttpConnectionManager.finishConnection(this);
+ mXmppConnectionService.updateConversationUi();
+ if (acceptedAutomatically) {
+ mXmppConnectionService.getNotificationService().push(message);
+ }
+ }
+
+ private void changeStatus(int status) {
+ this.mStatus = status;
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ private void setupTrustManager(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);
+ HttpConnection.this.acceptedAutomatically = false;
+ HttpConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ return;
+ } catch (IOException e) {
+ cancel();
+ return;
+ }
+ file.setExpectedSize(size);
+ if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
+ HttpConnection.this.acceptedAutomatically = true;
+ new Thread(new FileDownloader(interactive)).start();
+ } else {
+ changeStatus(STATUS_OFFER);
+ HttpConnection.this.acceptedAutomatically = false;
+ HttpConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ }
+ }
+
+ private long retrieveFileSize() throws IOException,
+ SSLHandshakeException {
+ changeStatus(STATUS_CHECKING);
+ HttpURLConnection connection = (HttpURLConnection) mUrl
+ .openConnection();
+ connection.setRequestMethod("HEAD");
+ if (connection instanceof HttpsURLConnection) {
+ setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ String contentLength = connection.getHeaderField("Content-Length");
+ if (contentLength == null) {
+ throw new IOException();
+ }
+ try {
+ return Long.parseLong(contentLength, 10);
+ } catch (NumberFormatException e) {
+ throw new IOException();
+ }
+ }
+
+ }
+
+ private class FileDownloader implements Runnable {
+
+ private boolean interactive = false;
+
+ public FileDownloader(boolean interactive) {
+ this.interactive = interactive;
+ }
+
+ @Override
+ public void run() {
+ try {
+ changeStatus(STATUS_DOWNLOADING);
+ download();
+ updateImageBounds();
+ finish();
+ } catch (SSLHandshakeException e) {
+ changeStatus(STATUS_OFFER);
+ } catch (IOException e) {
+ cancel();
+ }
+ }
+
+ private void download() throws SSLHandshakeException, IOException {
+ HttpURLConnection connection = (HttpURLConnection) mUrl
+ .openConnection();
+ if (connection instanceof HttpsURLConnection) {
+ setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ BufferedInputStream is = new BufferedInputStream(
+ connection.getInputStream());
+ 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..383ac89a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -0,0 +1,525 @@
+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 (message.getStatus() == Message.STATUS_RECEIVED
+ && conversation.getOtrSession() != null
+ && !conversation.getOtrSession().getSessionID().getUserID()
+ .equals(message.getPresence())) {
+ conversation.endOtrIfNeeded();
+ }
+
+ if (packet.getType() != MessagePacket.TYPE_ERROR) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE
+ || mXmppConnectionService.saveEncryptedMessages()) {
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ }
+ }
+ if (message.trusted() && message.bodyContainsDownloadable()) {
+ this.mXmppConnectionService.getHttpConnectionManager()
+ .createNewConnection(message);
+ notify = false;
+ }
+ 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..12e5e251
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -0,0 +1,353 @@
+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);
+ }
+
+ public List<Message> getImageMessages(Conversation conversation) {
+ ArrayList<Message> list = new ArrayList<Message>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor;
+ String[] selectionArgs = { conversation.getUuid(), String.valueOf(Message.TYPE_IMAGE) };
+ cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
+ + "=? AND "+Message.TYPE+"=?", selectionArgs, null, null,null);
+ if (cursor.getCount() > 0) {
+ cursor.moveToLast();
+ do {
+ Message message = Message.fromCursor(cursor);
+ message.setConversation(conversation);
+ list.add(message);
+ } while (cursor.moveToPrevious());
+ }
+ return list;
+ }
+}
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..13daa27b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -0,0 +1,447 @@
+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.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.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.ExifHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class FileBackend {
+
+ private static int IMAGE_SIZE = 1920;
+
+ private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
+ "yyyyMMdd_HHmmssSSS", Locale.US);
+
+ private XmppConnectionService mXmppConnectionService;
+
+ public FileBackend(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public DownloadableFile getFile(Message message) {
+ return getFile(message, true);
+ }
+
+ public DownloadableFile getFile(Message message, boolean decrypted) {
+ 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 {
+ try {
+ InputStream is = mXmppConnectionService.getContentResolver()
+ .openInputStream(image);
+ return ExifHelper.getOrientation(is);
+ } catch (FileNotFoundException e) {
+ return 0;
+ }
+ }
+ }
+
+ public Bitmap getImageFromMessage(Message message) {
+ return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
+ }
+
+ public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
+ throws FileNotFoundException {
+ Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
+ message.getUuid());
+ if ((thumbnail == null) && (!cacheOnly)) {
+ File file = getFile(message);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(file, size);
+ Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
+ options);
+ if (fullsize == null) {
+ throw new FileNotFoundException();
+ }
+ thumbnail = resize(fullsize, size);
+ this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
+ thumbnail);
+ }
+ return thumbnail;
+ }
+
+ public Uri getTakePhotoUri() {
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append(Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+ pathBuilder.append('/');
+ pathBuilder.append("Camera");
+ pathBuilder.append('/');
+ pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date())
+ + ".jpg");
+ Uri uri = Uri.parse("file://" + pathBuilder.toString());
+ File file = new File(uri.toString());
+ file.getParentFile().mkdirs();
+ return uri;
+ }
+
+ public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+ try {
+ Avatar avatar = new Avatar();
+ Bitmap bm = cropCenterSquare(image, size);
+ if (bm == null) {
+ return null;
+ }
+ ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
+ mByteArrayOutputStream, Base64.DEFAULT);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+ mBase64OutputSttream, digest);
+ if (!bm.compress(format, 75, mDigestOutputStream)) {
+ return null;
+ }
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ avatar.image = new String(mByteArrayOutputStream.toByteArray());
+ return avatar;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public boolean isAvatarCached(Avatar avatar) {
+ File file = new File(getAvatarPath(avatar.getFilename()));
+ return file.exists();
+ }
+
+ public boolean save(Avatar avatar) {
+ 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..bd27b555
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -0,0 +1,292 @@
+package eu.siacs.conversations.services;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.entities.MucOptions;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.net.Uri;
+
+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;
+ }
+ 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;
+ }
+ 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;
+ }
+ 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;
+ }
+ 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..7b2e16df
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -0,0 +1,333 @@
+package eu.siacs.conversations.services;
+
+import java.io.FileNotFoundException;
+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.graphics.Bitmap;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.BigPictureStyle;
+import android.support.v4.app.NotificationCompat.Builder;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.Html;
+import android.util.DisplayMetrics;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+
+public class NotificationService {
+
+ private XmppConnectionService mXmppConnectionService;
+
+ private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>();
+
+ public static int NOTIFICATION_ID = 0x2342;
+ private Conversation mOpenConversation;
+ private boolean mIsInForeground;
+ private long mLastNotification;
+
+ 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()
+ && !this.inMiniGracePeriod(account));
+ }
+
+ }
+
+ 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 {
+ if (notify) {
+ this.markLastNotification();
+ }
+ Builder mBuilder;
+ if (notifications.size() == 1) {
+ mBuilder = buildSingleConversations(notify);
+ } else {
+ mBuilder = buildMultipleConversation();
+ }
+ 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.setSmallIcon(R.drawable.ic_notification);
+ mBuilder.setDeleteIntent(createDeleteIntent());
+ mBuilder.setLights(0xffffffff, 2000, 4000);
+ Notification notification = mBuilder.build();
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ }
+
+ private Builder buildMultipleConversation() {
+ Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ 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> "
+ + getReadableBody(messages.get(0))));
+ 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()));
+ }
+ return mBuilder;
+ }
+
+ private Builder buildSingleConversations(boolean notify) {
+ Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ 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());
+ Message message;
+ if ((message = getImage(messages)) != null) {
+ modifyForImage(mBuilder, message, messages, notify);
+ } else {
+ modifyForTextOnly(mBuilder, messages, notify);
+ }
+ mBuilder.setContentIntent(createContentIntent(conversation
+ .getUuid()));
+ }
+ return mBuilder;
+
+ }
+
+ private void modifyForImage(Builder builder, Message message,
+ ArrayList<Message> messages, boolean notify) {
+ try {
+ Bitmap bitmap = mXmppConnectionService.getFileBackend()
+ .getThumbnail(message, getPixel(288), false);
+ ArrayList<Message> tmp = new ArrayList<Message>();
+ for (Message msg : messages) {
+ if (msg.getType() == Message.TYPE_TEXT
+ && msg.getDownloadable() == null) {
+ tmp.add(msg);
+ }
+ }
+ BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
+ bigPictureStyle.bigPicture(bitmap);
+ if (tmp.size() > 0) {
+ bigPictureStyle.setSummaryText(getMergedBodies(tmp));
+ builder.setContentText(getReadableBody(tmp.get(0)));
+ } else {
+ builder.setContentText(mXmppConnectionService.getString(R.string.image_file));
+ }
+ builder.setStyle(bigPictureStyle);
+ } catch (FileNotFoundException e) {
+ modifyForTextOnly(builder, messages, notify);
+ }
+ }
+
+ private void modifyForTextOnly(Builder builder,
+ ArrayList<Message> messages, boolean notify) {
+ builder.setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(getMergedBodies(messages)));
+ builder.setContentText(getReadableBody(messages.get(0)));
+ if (notify) {
+ builder.setTicker(getReadableBody(messages.get(messages.size() - 1)));
+ }
+ }
+
+ private Message getImage(ArrayList<Message> messages) {
+ for (Message message : messages) {
+ if (message.getType() == Message.TYPE_IMAGE
+ && message.getDownloadable() == null
+ && message.getEncryption() != Message.ENCRYPTION_PGP) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private String getMergedBodies(ArrayList<Message> messages) {
+ StringBuilder text = new StringBuilder();
+ for (int i = 0; i < messages.size(); ++i) {
+ text.append(getReadableBody(messages.get(i)));
+ if (i != messages.size() - 1) {
+ text.append("\n");
+ }
+ }
+ return text.toString();
+ }
+
+ private String getReadableBody(Message message) {
+ if (message.getDownloadable() != null
+ && (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message
+ .getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) {
+ return mXmppConnectionService.getText(
+ R.string.image_offered_for_download).toString();
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ return mXmppConnectionService.getText(
+ R.string.encrypted_message_received).toString();
+ } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ return mXmppConnectionService.getText(R.string.decryption_failed)
+ .toString();
+ } else if (message.getType() == Message.TYPE_IMAGE) {
+ return mXmppConnectionService.getText(R.string.image_file)
+ .toString();
+ } else {
+ return message.getBody().trim();
+ }
+ }
+
+ 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));
+ }
+
+ private void markLastNotification() {
+ this.mLastNotification = SystemClock.elapsedRealtime();
+ }
+
+ private boolean inMiniGracePeriod(Account account) {
+ int miniGrace = account.getStatus() == Account.STATUS_ONLINE ? Config.MINI_GRACE_PERIOD
+ : Config.MINI_GRACE_PERIOD * 2;
+ return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
+ }
+}
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..be73e07f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -0,0 +1,1949 @@
+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) {
+ if (online && contact.getPresences().size() > 1) {
+ conversation.endOtrIfNeeded();
+ } else {
+ conversation.resetOtrSession();
+ }
+ 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 {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ conv.startOtrIfNeeded();
+ }
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()
+ && (message.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) {
+ conv.startOtrIfNeeded();
+ 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() == account)
+ && (conversation.getContactJid().split("/", 2)[0]
+ .equalsIgnoreCase(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);
+ 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;
+ }
+
+ }
+
+ public void resendFailedMessages(Message message) {
+ List<Message> messages = new ArrayList<Message>();
+ Message current = message;
+ while(current.getStatus() == Message.STATUS_SEND_FAILED) {
+ messages.add(current);
+ if (current.mergable(current.next())) {
+ current = current.next();
+ } else {
+ break;
+ }
+ }
+ for(Message msg: messages) {
+ markMessage(msg, Message.STATUS_WAITING);
+ this.resendMessage(msg);
+ }
+ }
+}
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..f14da352
--- /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 = 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..52687c81
--- /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 = 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..4c52c609
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -0,0 +1,434 @@
+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 = 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 = 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..1d7364d6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -0,0 +1,936 @@
+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);
+ }
+ closeContextMenu();
+ }
+
+ @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 = menu.findItem(R.id.action_security);
+ MenuItem menuArchive = menu.findItem(R.id.action_archive);
+ MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
+ MenuItem menuContactDetails = menu
+ .findItem(R.id.action_contact_details);
+ MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
+ MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history);
+ MenuItem menuAdd = menu.findItem(R.id.action_add);
+ MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
+ MenuItem menuMute = 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 != null && VIEW_CONVERSATION.equals(intent.getType())) {
+ handleViewConversationIntent(intent);
+ }
+ } else {
+ 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 (getIntent() != null
+ && VIEW_CONVERSATION.equals(getIntent().getType())) {
+ handleViewConversationIntent(getIntent());
+ setIntent(null);
+ } else if (mOpenConverstaion != null) {
+ selectConversationByUuid(mOpenConverstaion);
+ paneShouldBeOpen = mPanelOpen;
+ if (paneShouldBeOpen) {
+ showConversationsOverview();
+ }
+ swapConversationFragment();
+ mOpenConverstaion = 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 handleViewConversationIntent(Intent intent) {
+ String uuid = (String) intent.getExtras().get(CONVERSATION);
+ String text = intent.getExtras().getString(TEXT, null);
+ selectConversationByUuid(uuid);
+ paneShouldBeOpen = false;
+ swapConversationFragment().setText(text);
+ }
+
+ 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..20eeeb30
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -0,0 +1,892 @@
+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.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.AbsListView;
+
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+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 Message selectedMessage;
+
+ 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);
+
+ registerForContextMenu(messagesView);
+
+ return view;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ this.selectedMessage = this.messageList.get(acmi.position);
+ populateContextMenu(menu);
+ }
+
+ private void populateContextMenu(ContextMenu menu) {
+ if (this.selectedMessage.getType() != Message.TYPE_STATUS) {
+ activity.getMenuInflater().inflate(R.menu.message_context, menu);
+ menu.setHeaderTitle(R.string.message_options);
+ MenuItem copyText = menu.findItem(R.id.copy_text);
+ MenuItem shareImage = menu.findItem(R.id.share_image);
+ MenuItem sendAgain = menu.findItem(R.id.send_again);
+ MenuItem copyUrl = menu.findItem(R.id.copy_url);
+ MenuItem downloadImage = menu.findItem(R.id.download_image);
+ if (this.selectedMessage.getType() != Message.TYPE_TEXT
+ || this.selectedMessage.getDownloadable() != null) {
+ copyText.setVisible(false);
+ }
+ if (this.selectedMessage.getType() != Message.TYPE_IMAGE
+ || this.selectedMessage.getDownloadable() != null) {
+ shareImage.setVisible(false);
+ }
+ if (this.selectedMessage.getStatus() != Message.STATUS_SEND_FAILED) {
+ sendAgain.setVisible(false);
+ }
+ if ((this.selectedMessage.getType() != Message.TYPE_IMAGE && this.selectedMessage
+ .getDownloadable() == null)
+ || this.selectedMessage.getImageParams().url == null) {
+ copyUrl.setVisible(false);
+ }
+
+ if (this.selectedMessage.getType() != Message.TYPE_TEXT
+ || this.selectedMessage.getDownloadable() != null
+ || !this.selectedMessage.bodyContainsDownloadable()) {
+ downloadImage.setVisible(false);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.share_image:
+ shareImage(selectedMessage);
+ return true;
+ case R.id.copy_text:
+ copyText(selectedMessage);
+ return true;
+ case R.id.send_again:
+ resendMessage(selectedMessage);
+ return true;
+ case R.id.copy_url:
+ copyUrl(selectedMessage);
+ return true;
+ case R.id.download_image:
+ downloadImage(selectedMessage);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private void shareImage(Message message) {
+ 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");
+ activity.startActivity(Intent.createChooser(shareIntent,
+ getText(R.string.share_with)));
+ }
+
+ private void copyText(Message message) {
+ if (activity.copyTextToClipboard(message.getMergedBody(),
+ R.string.message_text)) {
+ Toast.makeText(activity, R.string.message_copied_to_clipboard,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void resendMessage(Message message) {
+ activity.xmppConnectionService.resendFailedMessages(message);
+ }
+
+ private void copyUrl(Message message) {
+ if (activity.copyTextToClipboard(
+ message.getImageParams().url.toString(), R.string.image_url)) {
+ Toast.makeText(activity, R.string.url_copied_to_clipboard,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void downloadImage(Message message) {
+ activity.xmppConnectionService.getHttpConnectionManager()
+ .createNewConnection(message);
+ }
+
+ 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)
+ && message.getDownloadable() == null) {
+ 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();
+ }
+ } 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() {
+ Set<String> knownFingerprints = conversation.getContact()
+ .getOtrFingerprints();
+ if (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..58ca49cc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -0,0 +1,410 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.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 (copyTextToClipboard(fingerprint,R.string.otr_fingerprint)) {
+ Toast.makeText(
+ EditAccountActivity.this,
+ R.string.toast_message_otr_fingerprint,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ } else {
+ this.mOtrFingerprintBox.setVisibility(View.GONE);
+ }
+ } else {
+ if (this.mAccount.errorStatus()) {
+ this.mAccountJid.setError(getString(this.mAccount
+ .getReadableStatusId()));
+ this.mAccountJid.requestFocus();
+ }
+ this.mStats.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java
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..77f8b68a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -0,0 +1,218 @@
+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);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add_account:
+ startActivity(new Intent(getApplicationContext(),
+ EditAccountActivity.class));
+ break;
+ 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..416e926a
--- /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 = menu
+ .findItem(R.id.action_create_contact);
+ MenuItem menuCreateConference = menu
+ .findItem(R.id.action_join_conference);
+ mMenuSearchView = menu.findItem(R.id.action_search);
+ mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+ View mSearchView = mMenuSearchView.getActionView();
+ mSearchEditText = (EditText) mSearchView
+ .findViewById(R.id.search_field);
+ mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+ if (getActionBar().getSelectedNavigationIndex() == 0) {
+ menuCreateConference.setVisible(false);
+ } else {
+ menuCreateContact.setVisible(false);
+ }
+ if (mInitialJid != null) {
+ mMenuSearchView.expandActionView();
+ mSearchEditText.append(mInitialJid);
+ filter(mInitialJid);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_create_contact:
+ showCreateContactDialog(null);
+ 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..222f3295
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -0,0 +1,648 @@
+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.ClipData;
+import android.content.ClipboardManager;
+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 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 = 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 = 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 boolean copyTextToClipboard(String text,int labelResId) {
+ ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ String label = getResources().getString(labelResId);
+ if (mClipBoardManager != null) {
+ ClipData mClipData = ClipData.newPlainText(label, text);
+ mClipBoardManager.setPrimaryClip(mClipData);
+ return true;
+ }
+ return false;
+ }
+
+ 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..e13b3204
--- /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 = 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..b5c20dc5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -0,0 +1,139 @@
+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 = 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 (conversation.isRead()) {
+ mLastMessage.setTypeface(null, Typeface.ITALIC);
+ } else {
+ mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC);
+ }
+ if (d != null) {
+ mLastMessage.setVisibility(View.VISIBLE);
+ imagePreview.setVisibility(View.GONE);
+ 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 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ imagePreview.setVisibility(View.GONE);
+ mLastMessage.setVisibility(View.VISIBLE);
+ mLastMessage.setText(R.string.encrypted_message_received);
+ } 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..efc6b4d9
--- /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 = 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..a24f90d7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -0,0 +1,555 @@
+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;
+
+ private OnLongClickListener openContextMenu = new OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ v.showContextMenu();
+ return true;
+ }
+ };
+
+ 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";
+ }
+ if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) {
+ error = true;
+ }
+ }
+ switch (message.getMergedStatus()) {
+ case Message.STATUS_WAITING:
+ info = getContext().getString(R.string.waiting);
+ break;
+ case Message.STATUS_UNSEND:
+ info = getContext().getString(R.string.sending);
+ break;
+ case Message.STATUS_OFFERED:
+ info = getContext().getString(R.string.offering);
+ break;
+ case Message.STATUS_SEND_RECEIVED:
+ if (activity.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_DISPLAYED:
+ if (activity.indicateReceived()) {
+ viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+ }
+ break;
+ case Message.STATUS_SEND_FAILED:
+ info = getContext().getString(R.string.send_failed);
+ error = true;
+ break;
+ default:
+ if (multiReceived) {
+ 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);
+ }
+ });
+ viewHolder.download_button.setOnLongClickListener(openContextMenu);
+ }
+
+ private void displayImageMessage(ViewHolder viewHolder,
+ final Message message) {
+ if (viewHolder.download_button != null) {
+ viewHolder.download_button.setVisibility(View.GONE);
+ }
+ viewHolder.messageBody.setVisibility(View.GONE);
+ viewHolder.image.setVisibility(View.VISIBLE);
+ ImageParams params = message.getImageParams();
+ double target = metrics.density * 288;
+ int scalledW;
+ int scalledH;
+ if (params.width <= params.height) {
+ scalledW = (int) (params.width / ((double) params.height / target));
+ scalledH = (int) target;
+ } else {
+ scalledW = (int) target;
+ scalledH = (int) (params.height / ((double) params.width / target));
+ }
+ viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
+ scalledW, scalledH));
+ activity.loadBitmap(message, viewHolder.image);
+ viewHolder.image.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(activity.xmppConnectionService
+ .getFileBackend().getJingleFileUri(message), "image/*");
+ getContext().startActivity(intent);
+ }
+ });
+ viewHolder.image.setOnLongClickListener(openContextMenu);
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ final Message item = getItem(position);
+ int type = getItemViewType(position);
+ ViewHolder viewHolder;
+ if (view == null) {
+ viewHolder = new ViewHolder();
+ switch (type) {
+ case NULL:
+ view = activity.getLayoutInflater().inflate(
+ R.layout.message_null, parent, false);
+ break;
+ case SENT:
+ view = activity.getLayoutInflater().inflate(
+ R.layout.message_sent, parent, false);
+ viewHolder.message_box = (LinearLayout) view
+ .findViewById(R.id.message_box);
+ viewHolder.contact_picture = (ImageView) view
+ .findViewById(R.id.message_photo);
+ viewHolder.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 = 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 = 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 (d != null && d.getStatus() == Downloadable.STATUS_FAILED) {
+ displayInfoMessage(viewHolder, R.string.image_transmission_failed);
+ } 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..f101e883
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -0,0 +1,171 @@
+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("values")) {
+ return b;
+ } else if (b.containsKey("error")
+ && "nosrv".equals(b.getString("error", null))) {
+ return b;
+ }
+ }
+ }
+ return queryDNS(host, InetAddress.getByName("8.8.8.8"));
+ }
+
+ public static Bundle queryDNS(String host, InetAddress dnsServer) {
+ Bundle bundle = new Bundle();
+ try {
+ String qname = "_xmpp-client._tcp." + host;
+ Log.d(Config.LOGTAG,
+ "using dns server: " + dnsServer.getHostAddress()
+ + " to look up " + host);
+ DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN,
+ dnsServer.getHostAddress());
+
+ // How should we handle priorities and weight?
+ // Wikipedia has a nice article about priorities vs. weights:
+ // https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability
+
+ // we bucket the SRV records based on priority, pick per priority
+ // a random order respecting the weight, and dump that priority by
+ // priority
+
+ TreeMap<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) {
+ bundle.putString("error", "nosrv");
+ return bundle;
+ }
+ ArrayList<Bundle> values = new ArrayList<Bundle>();
+ for (SRV srv : result) {
+ Bundle namePort = new Bundle();
+ namePort.putString("name", srv.getName());
+ namePort.putInt("port", srv.getPort());
+ if (ips4.containsKey(srv.getName())) {
+ ArrayList<String> ip = ips4.get(srv.getName());
+ Collections.shuffle(ip, rnd);
+ namePort.putString("ipv4", ip.get(0));
+ }
+ values.add(namePort);
+ }
+ bundle.putParcelableArrayList("values", values);
+ } catch (SocketTimeoutException e) {
+ bundle.putString("error", "timeout");
+ } catch (Exception e) {
+ bundle.putString("error", "unhandled");
+ }
+ return bundle;
+ }
+
+ 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/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java
new file mode 100644
index 00000000..ceda7293
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.siacs.conversations.utils;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ExifHelper {
+ private static final String TAG = "CameraExif";
+
+ public static int getOrientation(InputStream is) {
+ if (is == null) {
+ return 0;
+ }
+
+ byte[] buf = new byte[8];
+ int length = 0;
+
+ // ISO/IEC 10918-1:1993(E)
+ while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) {
+ int marker = buf[1] & 0xFF;
+
+ // Check if the marker is a padding.
+ if (marker == 0xFF) {
+ continue;
+ }
+
+ // Check if the marker is SOI or TEM.
+ if (marker == 0xD8 || marker == 0x01) {
+ continue;
+ }
+ // Check if the marker is EOI or SOS.
+ if (marker == 0xD9 || marker == 0xDA) {
+ return 0;
+ }
+
+ // Get the length and check if it is reasonable.
+ if (!read(is, buf, 2)) {
+ return 0;
+ }
+ length = pack(buf, 0, 2, false);
+ if (length < 2) {
+ Log.e(TAG, "Invalid length");
+ return 0;
+ }
+ length -= 2;
+
+ // Break if the marker is EXIF in APP1.
+ if (marker == 0xE1 && length >= 6) {
+ if (!read(is, buf, 6)) return 0;
+ length -= 6;
+ if (pack(buf, 0, 4, false) == 0x45786966 &&
+ pack(buf, 4, 2, false) == 0) {
+ break;
+ }
+ }
+
+ // Skip other markers.
+ try {
+ is.skip(length);
+ } catch (IOException ex) {
+ return 0;
+ }
+ length = 0;
+ }
+
+ // JEITA CP-3451 Exif Version 2.2
+ if (length > 8) {
+ int offset = 0;
+ byte[] jpeg = new byte[length];
+ if (!read(is, jpeg, length)) {
+ return 0;
+ }
+
+ // Identify the byte order.
+ int tag = pack(jpeg, offset, 4, false);
+ if (tag != 0x49492A00 && tag != 0x4D4D002A) {
+ Log.e(TAG, "Invalid byte order");
+ return 0;
+ }
+ boolean littleEndian = (tag == 0x49492A00);
+
+ // Get the offset and check if it is reasonable.
+ int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
+ if (count < 10 || count > length) {
+ Log.e(TAG, "Invalid offset");
+ return 0;
+ }
+ offset += count;
+ length -= count;
+
+ // Get the count and go through all the elements.
+ count = pack(jpeg, offset - 2, 2, littleEndian);
+ while (count-- > 0 && length >= 12) {
+ // Get the tag and check if it is orientation.
+ tag = pack(jpeg, offset, 2, littleEndian);
+ if (tag == 0x0112) {
+ // We do not really care about type and count, do we?
+ int orientation = pack(jpeg, offset + 8, 2, littleEndian);
+ switch (orientation) {
+ case 1:
+ return 0;
+ case 3:
+ return 180;
+ case 6:
+ return 90;
+ case 8:
+ return 270;
+ }
+ Log.i(TAG, "Unsupported orientation");
+ return 0;
+ }
+ offset += 12;
+ length -= 12;
+ }
+ }
+
+ Log.i(TAG, "Orientation not found");
+ return 0;
+ }
+
+ private static int pack(byte[] bytes, int offset, int length,
+ boolean littleEndian) {
+ int step = 1;
+ if (littleEndian) {
+ offset += length - 1;
+ step = -1;
+ }
+
+ int value = 0;
+ while (length-- > 0) {
+ value = (value << 8) | (bytes[offset] & 0xFF);
+ offset += step;
+ }
+ return value;
+ }
+
+ private static boolean read(InputStream is, byte[] buf, int length) {
+ try {
+ return is.read(buf, 0, length) == length;
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/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..39dcb362
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -0,0 +1,1129 @@
+package eu.siacs.conversations.xmpp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.InetSocketAddress;
+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.Parcelable;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.util.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 result = DNSHelper.getSRVRecord(account.getServer());
+ ArrayList<Parcelable> values = result.getParcelableArrayList("values");
+ if ("timeout".equals(result.getString("error"))) {
+ Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
+ this.changeStatus(Account.STATUS_OFFLINE);
+ return;
+ } else if (values != null) {
+ int i = 0;
+ boolean socketError = true;
+ while (socketError && values.size() > i) {
+ Bundle namePort = (Bundle) values.get(i);
+ try {
+ String srvRecordServer = namePort.getString("name");
+ int srvRecordPort = namePort.getInt("port");
+ String srvIpServer = namePort.getString("ipv4");
+ InetSocketAddress addr;
+ if (srvIpServer!=null) {
+ addr = new InetSocketAddress(srvIpServer, srvRecordPort);
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": using values from dns " + srvRecordServer
+ + "[" + srvIpServer + "]:" + srvRecordPort);
+ } else {
+ addr = new InetSocketAddress(srvRecordServer, srvRecordPort);
+ Log.d(Config.LOGTAG, account.getJid()
+ + ": using values from dns "
+ + srvRecordServer + ":" + srvRecordPort);
+ }
+ socket = new Socket();
+ socket.connect(addr, 20000);
+ socketError = false;
+ } catch (UnknownHostException e) {
+ i++;
+ } catch (IOException e) {
+ i++;
+ }
+ }
+ if (socketError) {
+ throw new IOException();
+ }
+ } else if (result.containsKey("error")
+ && "nosrv".equals(result.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..6b9ca9aa
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -0,0 +1,902 @@
+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) {
+ cancel();
+ }
+ }
+ };
+
+ 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() {
+ this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED);
+ 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 {
+ 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..d937146a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -0,0 +1,168 @@
+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;
+ }
+ }
+ IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR);
+ Element error = response.addChild("error");
+ error.setAttribute("type", "cancel");
+ error.addChild("item-not-found",
+ "urn:ietf:params:xml:ns:xmpp-stanzas");
+ error.addChild("unknown-session", "urn:xmpp:jingle:errors:1");
+ account.getXmppConnection().sendIqPacket(response, null);
+ }
+ }
+
+ public JingleConnection createNewConnection(Message message) {
+ JingleConnection connection = new JingleConnection(this);
+ connection.init(message);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public JingleConnection createNewConnection(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..7c7ded57
--- /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..b463d788
--- /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..951d7aee
--- /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..39bb842a
--- /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"
+ android:longClickable="true">
+
+ <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: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..3e854643
--- /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"
+ android:longClickable="true">
+
+ <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: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/message_context.xml b/src/main/res/menu/message_context.xml
new file mode 100644
index 00000000..80d4d196
--- /dev/null
+++ b/src/main/res/menu/message_context.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/copy_text"
+ android:title="@string/copy_text"/>
+ <item
+ android:id="@+id/share_image"
+ android:title="@string/share_image"/>
+ <item
+ android:id="@+id/copy_url"
+ android:title="@string/copy_original_url"/>
+ <item
+ android:id="@+id/send_again"
+ android:title="@string/send_again"/>
+ <item
+ android:id="@+id/download_image"
+ android:title="@string/download_image"/>
+
+</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..6c68fe75
--- /dev/null
+++ b/src/main/res/values-cs/strings.xml
@@ -0,0 +1,265 @@
+<?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_indicate_received">Požadovat oznámení o přijetí</string>
+ <string name="pref_use_indicate_received_summary">Přijaté zprávy budou označeny zeleně. Toto nemusí vždy plně fungovat.</string>
+ <string name="pref_use_send_button_to_indicate_status_summary">Obarvit tlačítko pro odeslání barvou stavu kontaktu</string>
+ <string name="pref_expert_options_other">Další</string>
+ <string name="pref_conference_name">Jméno konference</string>
+ <string name="pref_conference_name_summary">Pro identifikaci konferencí použít téma místnosti místo jejího JID</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..ed5d47b5
--- /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>1 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..ca190deb
--- /dev/null
+++ b/src/main/res/values-de/strings.xml
@@ -0,0 +1,285 @@
+<?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 Konten</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">Ressource des Kontakts auswählen</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 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 deine Kontakte ihre Schlüssel nicht preisgeben.\n\n<small>Bitte sag deinen Kontakten, sie mögen 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, dass dies nicht 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_enable_legacy_ssl">Alte SSL-Version aktivieren</string>
+ <string name="pref_enable_legacy_ssl_summary">Aktiviert SSLv3-Unterstützung für alte Server. Achtung: SSLv3 ist unsicher.</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">Schrift vergrößern</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>
+ <string name="using_account">Verwende Konto %s</string>
+ <string name="checking_image">Prüfe Bild auf HTTP-Host</string>
+ <string name="image_file_deleted">Bilddatei wurde gelöscht</string>
+ <string name="not_connected_try_again">Nicht verbunden, bitte später versuchen</string>
+ <string name="check_image_filesize">Bildgröße prüfen</string>
+ <string name="message_options">Nachrichtenoptionen</string>
+ <string name="copy_text">Text kopieren</string>
+ <string name="share_image">Bild teilen</string>
+ <string name="copy_original_url">Original-URL kopieren</string>
+ <string name="send_again">Erneut senden</string>
+ <string name="image_url">Bild-URL</string>
+ <string name="message_text">Nachrichtentext</string>
+ <string name="url_copied_to_clipboard">URL in Zwischenablage kopiert</string>
+ <string name="message_copied_to_clipboard">Nachricht in Zwischenablage kopiert</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..47424d00
--- /dev/null
+++ b/src/main/res/values-es/strings.xml
@@ -0,0 +1,285 @@
+<?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_enable_legacy_ssl">Habilitar SSL heredado</string>
+ <string name="pref_enable_legacy_ssl_summary">Habilita soporte SSLv3 para servidores heredados. Advertencia: SSLv3 se considera no seguro.</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>
+ <string name="using_account">Usando cuenta %s</string>
+ <string name="checking_image">Comprobando imagen en servidor HTTP</string>
+ <string name="image_file_deleted">El archivo de imagen ha sido eliminado</string>
+ <string name="not_connected_try_again">No estás concectado. Inténtalo más tarde</string>
+ <string name="check_image_filesize">Comprobar el tamaño del archivo de imagen</string>
+ <string name="message_options">Opciones de mensaje</string>
+ <string name="copy_text">Copiar texto</string>
+ <string name="share_image">Compartir imagen</string>
+ <string name="copy_original_url">Copiar URL original</string>
+ <string name="send_again">Volver a enviar</string>
+ <string name="image_url">URL Imagen</string>
+ <string name="message_text">Mensaje de texto</string>
+ <string name="url_copied_to_clipboard">URL copiada al portapapeles</string>
+ <string name="message_copied_to_clipboard">Mensaje copiado al portapapeles</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..8896e9b5
--- /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..4acc9e62
--- /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>1 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>
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..e941ed6d
--- /dev/null
+++ b/src/main/res/values/strings.xml
@@ -0,0 +1,286 @@
+<?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>
+ <string name="message_options">Message options</string>
+ <string name="copy_text">Copy text</string>
+ <string name="share_image">Share image</string>
+ <string name="copy_original_url">Copy original URL</string>
+ <string name="send_again">Send again</string>
+ <string name="image_url">Image URL</string>
+ <string name="message_text">Message text</string>
+ <string name="url_copied_to_clipboard">URL copied to clipboard</string>
+ <string name="message_copied_to_clipboard">Message copied to clipboard</string>
+ <string name="image_transmission_failed">Image transmission failed</string>
+
+</resources> \ No newline at end of file
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>