Use Gradle build system

Sam Whited created

Change summary

.gitignore                                                                                          |   41 
.gitmodules                                                                                         |    9 
build.gradle                                                                                        |   16 
conversations/build.gradle                                                                          |   28 
conversations/src/main/AndroidManifest.xml                                                          |  124 
conversations/src/main/java/eu/siacs/conversations/Config.java                                      |   25 
conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java                            |  231 
conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java                            |  385 
conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java                     |   21 
conversations/src/main/java/eu/siacs/conversations/entities/Account.java                            |  399 
conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java                           |  137 
conversations/src/main/java/eu/siacs/conversations/entities/Contact.java                            |  367 
conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java                       |  500 
conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java                       |   21 
conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java                   |  154 
conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java                           |    7 
conversations/src/main/java/eu/siacs/conversations/entities/Message.java                            |  478 
conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java                         |  369 
conversations/src/main/java/eu/siacs/conversations/entities/Presences.java                          |   76 
conversations/src/main/java/eu/siacs/conversations/entities/Roster.java                             |   83 
conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java                 |   48 
conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java                       |   96 
conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java                  |  178 
conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java                 |   57 
conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java                         |  255 
conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java                  |   28 
conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java                       |   92 
conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java                             |   92 
conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java                        |  517 
conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java                       |  133 
conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java                 |  335 
conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java                     |  480 
conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java           |    5 
conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java          |   23 
conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java                      |  298 
conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java                      |   24 
conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java                |  237 
conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java              | 1927 
conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java                    |  145 
conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java                |  280 
conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java                   |  436 
conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java                     |  947 
conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java                     |  781 
conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java                      |  423 
conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java                              |   39 
conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java                    |  217 
conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java            |  242 
conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java                         |   74 
conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java                         |   15 
conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java                        |  185 
conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java                |  677 
conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java                               |   11 
conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java                             |  637 
conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java                   |  102 
conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java              |  135 
conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java                |   74 
conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java                  |   44 
conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java                   |  560 
conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java                          |  112 
conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java                             |  185 
conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java                      |   44 
conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java                       |  117 
conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java         |    9 
conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java                             |  327 
conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java                           |   95 
conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java                              |  225 
conversations/src/main/java/eu/siacs/conversations/utils/Validator.java                             |   14 
conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java                             |   12 
conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java                  |   54 
conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java                 |   95 
conversations/src/main/java/eu/siacs/conversations/xml/Element.java                                 |  148 
conversations/src/main/java/eu/siacs/conversations/xml/Tag.java                                     |  104 
conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java                               |  114 
conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java                               |  141 
conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java                         |    7 
conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java                 |    7 
conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java                     |    8 
conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java                  |    7 
conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java                |    8 
conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java               |    8 
conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java                        |    7 
conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java                         |    5 
conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                         | 1130 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java                 |  143 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java                |  910 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java         |  163 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java           |  191 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java           |  212 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java                 |   13 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java |    9 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java          |    9 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java         |    6 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java            |    7 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java                 |  102 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java            |   95 
conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java                  |   13 
conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java                             |   71 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java                 |   34 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java                       |   76 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java                  |   66 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java                 |    8 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java               |   10 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java             |   10 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java           |   13 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java        |   13 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java       |   12 
conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java        |   14 
conversations/src/main/res/drawable-hdpi/ic_action_add_group.png                                    |    0 
conversations/src/main/res/drawable-hdpi/ic_action_add_person.png                                   |    0 
conversations/src/main/res/drawable-hdpi/ic_action_chat.png                                         |    0 
conversations/src/main/res/drawable-hdpi/ic_action_copy.png                                         |    0 
conversations/src/main/res/drawable-hdpi/ic_action_discard.png                                      |    0 
conversations/src/main/res/drawable-hdpi/ic_action_edit.png                                         |    0 
conversations/src/main/res/drawable-hdpi/ic_action_edit_dark.png                                    |    0 
conversations/src/main/res/drawable-hdpi/ic_action_group.png                                        |    0 
conversations/src/main/res/drawable-hdpi/ic_action_new.png                                          |    0 
conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png                               |    0 
conversations/src/main/res/drawable-hdpi/ic_action_not_secure.png                                   |    0 
conversations/src/main/res/drawable-hdpi/ic_action_refresh.png                                      |    0 
conversations/src/main/res/drawable-hdpi/ic_action_remove.png                                       |    0 
conversations/src/main/res/drawable-hdpi/ic_action_search.png                                       |    0 
conversations/src/main/res/drawable-hdpi/ic_action_secure.png                                       |    0 
conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png                                |    0 
conversations/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png                                 |    0 
conversations/src/main/res/drawable-hdpi/ic_action_send_now_offline.png                             |    0 
conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png                              |    0 
conversations/src/main/res/drawable-hdpi/ic_activity.png                                            |    0 
conversations/src/main/res/drawable-hdpi/ic_indicator.png                                           |    0 
conversations/src/main/res/drawable-hdpi/ic_launcher.png                                            |    0 
conversations/src/main/res/drawable-hdpi/ic_notification.png                                        |    0 
conversations/src/main/res/drawable-hdpi/ic_profile.png                                             |    0 
conversations/src/main/res/drawable-hdpi/ic_received_indicator.png                                  |    0 
conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png                                    |    0 
conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png                           |    0 
conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png                   |    0 
conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png                   |    0 
conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png                         |    0 
conversations/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png                 |    0 
conversations/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png                 |    0 
conversations/src/main/res/drawable-mdpi/ic_action_add_group.png                                    |    0 
conversations/src/main/res/drawable-mdpi/ic_action_add_person.png                                   |    0 
conversations/src/main/res/drawable-mdpi/ic_action_chat.png                                         |    0 
conversations/src/main/res/drawable-mdpi/ic_action_copy.png                                         |    0 
conversations/src/main/res/drawable-mdpi/ic_action_discard.png                                      |    0 
conversations/src/main/res/drawable-mdpi/ic_action_edit.png                                         |    0 
conversations/src/main/res/drawable-mdpi/ic_action_edit_dark.png                                    |    0 
conversations/src/main/res/drawable-mdpi/ic_action_group.png                                        |    0 
conversations/src/main/res/drawable-mdpi/ic_action_new.png                                          |    0 
conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png                               |    0 
conversations/src/main/res/drawable-mdpi/ic_action_not_secure.png                                   |    0 
conversations/src/main/res/drawable-mdpi/ic_action_refresh.png                                      |    0 
conversations/src/main/res/drawable-mdpi/ic_action_remove.png                                       |    0 
conversations/src/main/res/drawable-mdpi/ic_action_search.png                                       |    0 
conversations/src/main/res/drawable-mdpi/ic_action_secure.png                                       |    0 
conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png                                |    0 
conversations/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png                                 |    0 
conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png                             |    0 
conversations/src/main/res/drawable-mdpi/ic_action_send_now_online.png                              |    0 
conversations/src/main/res/drawable-mdpi/ic_activity.png                                            |    0 
conversations/src/main/res/drawable-mdpi/ic_indicator.png                                           |    0 
conversations/src/main/res/drawable-mdpi/ic_launcher.png                                            |    0 
conversations/src/main/res/drawable-mdpi/ic_notification.png                                        |    0 
conversations/src/main/res/drawable-mdpi/ic_profile.png                                             |    0 
conversations/src/main/res/drawable-mdpi/ic_received_indicator.png                                  |    0 
conversations/src/main/res/drawable-mdpi/ic_secure_indicator.png                                    |    0 
conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png                           |    0 
conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png                   |    0 
conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png                   |    0 
conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png                         |    0 
conversations/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png                 |    0 
conversations/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png                 |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png                                   |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png                                  |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_chat.png                                        |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_copy.png                                        |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_discard.png                                     |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_edit.png                                        |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png                                   |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_group.png                                       |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_new.png                                         |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png                              |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png                                  |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png                                     |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_remove.png                                      |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_search.png                                      |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_secure.png                                      |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png                               |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png                                |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png                            |    0 
conversations/src/main/res/drawable-xhdpi/ic_action_send_now_online.png                             |    0 
conversations/src/main/res/drawable-xhdpi/ic_activity.png                                           |    0 
conversations/src/main/res/drawable-xhdpi/ic_indicator.png                                          |    0 
conversations/src/main/res/drawable-xhdpi/ic_launcher.png                                           |    0 
conversations/src/main/res/drawable-xhdpi/ic_notification.png                                       |    0 
conversations/src/main/res/drawable-xhdpi/ic_profile.png                                            |    0 
conversations/src/main/res/drawable-xhdpi/ic_received_indicator.png                                 |    0 
conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png                                   |    0 
conversations/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png                          |    0 
conversations/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png                  |    0 
conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png                  |    0 
conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png                        |    0 
conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png                |    0 
conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png                |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png                                  |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png                                 |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_chat.png                                       |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_copy.png                                       |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png                                    |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_edit.png                                       |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png                                  |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_group.png                                      |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_new.png                                        |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png                             |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_not_secure.png                                 |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png                                    |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png                                     |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_search.png                                     |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_secure.png                                     |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png                              |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png                               |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png                           |    0 
conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png                            |    0 
conversations/src/main/res/drawable-xxhdpi/ic_activity.png                                          |    0 
conversations/src/main/res/drawable-xxhdpi/ic_indicator.png                                         |    0 
conversations/src/main/res/drawable-xxhdpi/ic_launcher.png                                          |    0 
conversations/src/main/res/drawable-xxhdpi/ic_notification.png                                      |    0 
conversations/src/main/res/drawable-xxhdpi/ic_profile.png                                           |    0 
conversations/src/main/res/drawable-xxhdpi/ic_received_indicator.png                                |    0 
conversations/src/main/res/drawable-xxhdpi/ic_secure_indicator.png                                  |    0 
conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png                         |    0 
conversations/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png                 |    0 
conversations/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png                 |    0 
conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png                       |    0 
conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png               |    0 
conversations/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png               |    0 
conversations/src/main/res/drawable/actionbar_tab_indicator.xml                                     |   21 
conversations/src/main/res/drawable/es_slidingpane_shadow.xml                                       |   12 
conversations/src/main/res/drawable/grey.xml                                                        |    7 
conversations/src/main/res/drawable/greybackground.xml                                              |    6 
conversations/src/main/res/drawable/infocard_border.xml                                             |   19 
conversations/src/main/res/drawable/message_border.xml                                              |   15 
conversations/src/main/res/drawable/snackbar.xml                                                    |   14 
conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml                        |   30 
conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml                        |   30 
conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml                        |   30 
conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml                        |   32 
conversations/src/main/res/layout/account_row.xml                                                   |   43 
conversations/src/main/res/layout/actionview_search.xml                                             |   19 
conversations/src/main/res/layout/activity_choose_contact.xml                                       |   13 
conversations/src/main/res/layout/activity_contact_details.xml                                      |  114 
conversations/src/main/res/layout/activity_edit_account.xml                                         |  272 
conversations/src/main/res/layout/activity_muc_details.xml                                          |  119 
conversations/src/main/res/layout/activity_publish_profile_picture.xml                              |  106 
conversations/src/main/res/layout/activity_start_conversation.xml                                   |    8 
conversations/src/main/res/layout/contact.xml                                                       |   51 
conversations/src/main/res/layout/contact_key.xml                                                   |   41 
conversations/src/main/res/layout/conversation_list_row.xml                                         |   68 
conversations/src/main/res/layout/create_contact_dialog.xml                                         |   39 
conversations/src/main/res/layout/dialog_clear_history.xml                                          |   21 
conversations/src/main/res/layout/dialog_verify_otr.xml                                             |   60 
conversations/src/main/res/layout/fragment_conversation.xml                                         |  102 
conversations/src/main/res/layout/fragment_conversations_overview.xml                               |   30 
conversations/src/main/res/layout/join_conference_dialog.xml                                        |   47 
conversations/src/main/res/layout/manage_accounts.xml                                               |   16 
conversations/src/main/res/layout/message_null.xml                                                  |    7 
conversations/src/main/res/layout/message_received.xml                                              |   97 
conversations/src/main/res/layout/message_sent.xml                                                  |  108 
conversations/src/main/res/layout/message_status.xml                                                |   22 
conversations/src/main/res/layout/quickedit.xml                                                     |   19 
conversations/src/main/res/layout/share_with.xml                                                    |   13 
conversations/src/main/res/menu/attachment_choices.xml                                              |   15 
conversations/src/main/res/menu/choose_contact.xml                                                  |   11 
conversations/src/main/res/menu/conference_context.xml                                              |   11 
conversations/src/main/res/menu/contact_context.xml                                                 |   14 
conversations/src/main/res/menu/contact_details.xml                                                 |   27 
conversations/src/main/res/menu/conversations.xml                                                   |   63 
conversations/src/main/res/menu/encryption_choices.xml                                              |   16 
conversations/src/main/res/menu/manageaccounts.xml                                                  |   15 
conversations/src/main/res/menu/manageaccounts_context.xml                                          |   21 
conversations/src/main/res/menu/muc_details.xml                                                     |   21 
conversations/src/main/res/menu/share_with.xml                                                      |   11 
conversations/src/main/res/menu/start_conversation.xml                                              |   31 
conversations/src/main/res/values-ca/arrays.xml                                                     |   24 
conversations/src/main/res/values-ca/strings.xml                                                    |   83 
conversations/src/main/res/values-cs/arrays.xml                                                     |   39 
conversations/src/main/res/values-cs/strings.xml                                                    |  260 
conversations/src/main/res/values-de/arrays.xml                                                     |   31 
conversations/src/main/res/values-de/strings.xml                                                    |  269 
conversations/src/main/res/values-es/arrays.xml                                                     |   39 
conversations/src/main/res/values-es/strings.xml                                                    |  269 
conversations/src/main/res/values-eu/arrays.xml                                                     |   39 
conversations/src/main/res/values-eu/strings.xml                                                    |  276 
conversations/src/main/res/values-fr/arrays.xml                                                     |   24 
conversations/src/main/res/values-fr/strings.xml                                                    |  273 
conversations/src/main/res/values-gl/arrays.xml                                                     |   24 
conversations/src/main/res/values-gl/strings.xml                                                    |  130 
conversations/src/main/res/values-it/arrays.xml                                                     |   39 
conversations/src/main/res/values-it/strings.xml                                                    |  260 
conversations/src/main/res/values-iw/arrays.xml                                                     |   24 
conversations/src/main/res/values-iw/strings.xml                                                    |  224 
conversations/src/main/res/values-nl/arrays.xml                                                     |   24 
conversations/src/main/res/values-nl/strings.xml                                                    |  233 
conversations/src/main/res/values-ru/arrays.xml                                                     |   24 
conversations/src/main/res/values-ru/strings.xml                                                    |  260 
conversations/src/main/res/values-sv/arrays.xml                                                     |   24 
conversations/src/main/res/values-sv/strings.xml                                                    |  260 
conversations/src/main/res/values-zh-rCN/arrays.xml                                                 |   39 
conversations/src/main/res/values-zh-rCN/strings.xml                                                |  260 
conversations/src/main/res/values-zh-rTW/arrays.xml                                                 |   39 
conversations/src/main/res/values-zh-rTW/strings.xml                                                |  263 
conversations/src/main/res/values/arrays.xml                                                        |   39 
conversations/src/main/res/values/attrs.xml                                                         |    8 
conversations/src/main/res/values/colors.xml                                                        |   17 
conversations/src/main/res/values/strings.xml                                                       |  276 
conversations/src/main/res/values/styles.xml                                                        |    8 
conversations/src/main/res/values/themes.xml                                                        |   35 
conversations/src/main/res/xml/preferences.xml                                                      |  114 
gradlew                                                                                             |  164 
gradlew.bat                                                                                         |   90 
memorizingTrustManager                                                                              |    1 
minidns                                                                                             |    1 
openpgpapilib                                                                                       |    1 
settings.gradle                                                                                     |    4 
323 files changed, 25,921 insertions(+)

Detailed changes

.gitignore 🔗

@@ -0,0 +1,41 @@
+.classpath
+*.swp
+.settings
+
+# https://github.com/github/gitignore/blob/master/Gradle.gitignore
+.gradle/
+gradle/
+build/
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# https://github.com/github/gitignore/blob/master/Android.gitignore
+# Built application files
+*.apk
+*.ap_
+
+# Files for the Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+*.iml
+.idea
+
+import-summary.txt
+
+*.jar

.gitmodules 🔗

@@ -0,0 +1,9 @@
+[submodule "minidns"]
+	path = minidns
+	url = https://github.com/rtreffer/minidns.git
+[submodule "openpgpapilib"]
+	path = openpgpapilib
+	url = https://github.com/open-keychain/openpgp-api-lib.git
+[submodule "memorizingTrustManager"]
+	path = memorizingTrustManager
+	url = https://github.com/iNPUTmice/MemorizingTrustManager.git

build.gradle 🔗

@@ -0,0 +1,16 @@
+// Top-level build file where you can add configuration options common to all
+// sub-projects/modules.
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:0.12.2'
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+    }
+}

conversations/build.gradle 🔗

@@ -0,0 +1,28 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 19
+    buildToolsVersion "20.0.0"
+
+    defaultConfig {
+        applicationId "eu.siacs.conversations"
+        minSdkVersion 14
+        targetSdkVersion 19
+    }
+
+    buildTypes {
+        release {
+            runProguard false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+        }
+    }
+}
+
+dependencies {
+    compile project(':minidns')
+    compile project(':openpgpapilib')
+    compile project(':memorizingTrustManager')
+    compile files('libs/android-support-v13.jar')
+    compile files('libs/bcprov-jdk15on-150.jar')
+    compile files('libs/otr4j-0.10.jar')
+}

conversations/src/main/AndroidManifest.xml 🔗

@@ -0,0 +1,124 @@
+<?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"
+    android:versionCode="32"
+    android:versionName="0.8-alpha" >
+
+    <uses-sdk
+        android:minSdkVersion="14"
+        android:targetSdkVersion="19" />
+
+    <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>

conversations/src/main/java/eu/siacs/conversations/Config.java 🔗

@@ -0,0 +1,25 @@
+package eu.siacs.conversations;
+
+import android.graphics.Bitmap;
+
+public final class Config {
+
+	public static final String LOGTAG = "conversations";
+
+	public static final int PING_MAX_INTERVAL = 300;
+	public static final int PING_MIN_INTERVAL = 30;
+	public static final int PING_TIMEOUT = 10;
+	public static final int CONNECT_TIMEOUT = 90;
+	public static final int CARBON_GRACE_PERIOD = 60;
+
+	public static final int AVATAR_SIZE = 192;
+	public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
+
+	public static final int MESSAGE_MERGE_WINDOW = 20;
+
+	public static final boolean PARSE_EMOTICONS = false;
+
+	private Config() {
+
+	}
+}

conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java 🔗

@@ -0,0 +1,231 @@
+package eu.siacs.conversations.crypto;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.session.InstanceTag;
+import net.java.otr4j.session.SessionID;
+
+public class OtrEngine implements OtrEngineHost {
+
+	private Account account;
+	private OtrPolicy otrPolicy;
+	private KeyPair keyPair;
+	private XmppConnectionService mXmppConnectionService;
+
+	public OtrEngine(XmppConnectionService service, Account account) {
+		this.account = account;
+		this.otrPolicy = new OtrPolicyImpl();
+		this.otrPolicy.setAllowV1(false);
+		this.otrPolicy.setAllowV2(true);
+		this.otrPolicy.setAllowV3(true);
+		this.keyPair = loadKey(account.getKeys());
+		this.mXmppConnectionService = service;
+	}
+
+	private KeyPair loadKey(JSONObject keys) {
+		if (keys == null) {
+			return null;
+		}
+		try {
+			BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
+			BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
+			BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
+			BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
+			BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
+			KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+			DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
+			DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
+			PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
+			PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
+			return new KeyPair(publicKey, privateKey);
+		} catch (JSONException e) {
+			return null;
+		} catch (NoSuchAlgorithmException e) {
+			return null;
+		} catch (InvalidKeySpecException e) {
+			return null;
+		}
+	}
+
+	private void saveKey() {
+		PublicKey publicKey = keyPair.getPublic();
+		PrivateKey privateKey = keyPair.getPrivate();
+		KeyFactory keyFactory;
+		try {
+			keyFactory = KeyFactory.getInstance("DSA");
+			DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(
+					privateKey, DSAPrivateKeySpec.class);
+			DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey,
+					DSAPublicKeySpec.class);
+			this.account.setKey("otr_x", privateKeySpec.getX().toString(16));
+			this.account.setKey("otr_g", privateKeySpec.getG().toString(16));
+			this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
+			this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
+			this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
+		} catch (NoSuchAlgorithmException e) {
+			e.printStackTrace();
+		} catch (InvalidKeySpecException e) {
+			e.printStackTrace();
+		}
+
+	}
+
+	@Override
+	public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void finishedSessionMessage(SessionID arg0, String arg1)
+			throws OtrException {
+
+	}
+
+	@Override
+	public String getFallbackMessage(SessionID arg0) {
+		return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
+	}
+
+	@Override
+	public byte[] getLocalFingerprintRaw(SessionID arg0) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	public PublicKey getPublicKey() {
+		if (this.keyPair == null) {
+			return null;
+		}
+		return this.keyPair.getPublic();
+	}
+
+	@Override
+	public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
+		if (this.keyPair == null) {
+			KeyPairGenerator kg;
+			try {
+				kg = KeyPairGenerator.getInstance("DSA");
+				this.keyPair = kg.genKeyPair();
+				this.saveKey();
+				mXmppConnectionService.databaseBackend.updateAccount(account);
+			} catch (NoSuchAlgorithmException e) {
+				Log.d(Config.LOGTAG,
+						"error generating key pair " + e.getMessage());
+			}
+		}
+		return this.keyPair;
+	}
+
+	@Override
+	public String getReplyForUnreadableMessage(SessionID arg0) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	@Override
+	public OtrPolicy getSessionPolicy(SessionID arg0) {
+		return otrPolicy;
+	}
+
+	@Override
+	public void injectMessage(SessionID session, String body)
+			throws OtrException {
+		MessagePacket packet = new MessagePacket();
+		packet.setFrom(account.getFullJid());
+		if (session.getUserID().isEmpty()) {
+			packet.setTo(session.getAccountID());
+		} else {
+			packet.setTo(session.getAccountID() + "/" + session.getUserID());
+		}
+		packet.setBody(body);
+		packet.addChild("private", "urn:xmpp:carbons:2");
+		packet.addChild("no-copy", "urn:xmpp:hints");
+		packet.setType(MessagePacket.TYPE_CHAT);
+		account.getXmppConnection().sendMessagePacket(packet);
+	}
+
+	@Override
+	public void messageFromAnotherInstanceReceived(SessionID id) {
+		Log.d(Config.LOGTAG,
+				"unreadable message received from " + id.getAccountID());
+	}
+
+	@Override
+	public void multipleInstancesDetected(SessionID arg0) {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void requireEncryptedMessage(SessionID arg0, String arg1)
+			throws OtrException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void showError(SessionID arg0, String arg1) throws OtrException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void smpAborted(SessionID arg0) throws OtrException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void smpError(SessionID arg0, int arg1, boolean arg2)
+			throws OtrException {
+		throw new OtrException(new Exception("smp error"));
+	}
+
+	@Override
+	public void unencryptedMessageReceived(SessionID arg0, String arg1)
+			throws OtrException {
+		throw new OtrException(new Exception("unencrypted message received"));
+	}
+
+	@Override
+	public void unreadableMessageReceived(SessionID arg0) throws OtrException {
+		throw new OtrException(new Exception("unreadable message received"));
+	}
+
+	@Override
+	public void unverify(SessionID arg0, String arg1) {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void verify(SessionID arg0, String arg1, boolean arg2) {
+		// TODO Auto-generated method stub
+
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java 🔗

@@ -0,0 +1,385 @@
+package eu.siacs.conversations.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.UiCallback;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+public class PgpEngine {
+	private OpenPgpApi api;
+	private XmppConnectionService mXmppConnectionService;
+
+	public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
+		this.api = api;
+		this.mXmppConnectionService = service;
+	}
+
+	public void decrypt(final Message message,
+			final UiCallback<Message> callback) {
+		Log.d(Config.LOGTAG, "decrypting message " + message.getUuid());
+		Intent params = new Intent();
+		params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+		params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message
+				.getConversation().getAccount().getJid());
+		if (message.getType() == Message.TYPE_TEXT) {
+			InputStream is = new ByteArrayInputStream(message.getBody()
+					.getBytes());
+			final OutputStream os = new ByteArrayOutputStream();
+			api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+				@Override
+				public void onReturn(Intent result) {
+					switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+							OpenPgpApi.RESULT_CODE_ERROR)) {
+					case OpenPgpApi.RESULT_CODE_SUCCESS:
+						try {
+							os.flush();
+							if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+								message.setBody(os.toString());
+								message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+								callback.success(message);
+							}
+						} catch (IOException e) {
+							callback.error(R.string.openpgp_error, message);
+							return;
+						}
+
+						return;
+					case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+						callback.userInputRequried((PendingIntent) result
+								.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+								message);
+						return;
+					case OpenPgpApi.RESULT_CODE_ERROR:
+						OpenPgpError error = result
+								.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
+						Log.d(Config.LOGTAG,
+								"openpgp error: " + error.getMessage());
+						callback.error(R.string.openpgp_error, message);
+						return;
+					default:
+						return;
+					}
+				}
+			});
+		} else if (message.getType() == Message.TYPE_IMAGE) {
+			try {
+				final DownloadableFile inputFile = this.mXmppConnectionService
+						.getFileBackend().getFile(message, false);
+				final DownloadableFile outputFile = this.mXmppConnectionService
+						.getFileBackend().getFile(message, true);
+				outputFile.createNewFile();
+				InputStream is = new FileInputStream(inputFile);
+				OutputStream os = new FileOutputStream(outputFile);
+				api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+					@Override
+					public void onReturn(Intent result) {
+						switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+								OpenPgpApi.RESULT_CODE_ERROR)) {
+						case OpenPgpApi.RESULT_CODE_SUCCESS:
+							BitmapFactory.Options options = new BitmapFactory.Options();
+							options.inJustDecodeBounds = true;
+							BitmapFactory.decodeFile(
+									outputFile.getAbsolutePath(), options);
+							int imageHeight = options.outHeight;
+							int imageWidth = options.outWidth;
+							message.setBody(Long.toString(outputFile.getSize())
+									+ ',' + imageWidth + ',' + imageHeight);
+							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+							PgpEngine.this.mXmppConnectionService
+									.updateMessage(message);
+							;
+							callback.success(message);
+							return;
+						case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+							callback.userInputRequried(
+									(PendingIntent) result
+											.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+									message);
+							return;
+						case OpenPgpApi.RESULT_CODE_ERROR:
+							callback.error(R.string.openpgp_error, message);
+							return;
+						default:
+							return;
+						}
+					}
+				});
+			} catch (FileNotFoundException e) {
+				callback.error(R.string.error_decrypting_file, message);
+			} catch (IOException e) {
+				callback.error(R.string.error_decrypting_file, message);
+			}
+
+		}
+	}
+
+	public void encrypt(final Message message,
+			final UiCallback<Message> callback) {
+
+		Intent params = new Intent();
+		params.setAction(OpenPgpApi.ACTION_ENCRYPT);
+		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
+			long[] keys = { message.getConversation().getContact()
+					.getPgpKeyId() };
+			params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
+		} else {
+			params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation()
+					.getMucOptions().getPgpKeyIds());
+		}
+		params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message
+				.getConversation().getAccount().getJid());
+
+		if (message.getType() == Message.TYPE_TEXT) {
+			params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+
+			InputStream is = new ByteArrayInputStream(message.getBody()
+					.getBytes());
+			final OutputStream os = new ByteArrayOutputStream();
+			api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+				@Override
+				public void onReturn(Intent result) {
+					switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+							OpenPgpApi.RESULT_CODE_ERROR)) {
+					case OpenPgpApi.RESULT_CODE_SUCCESS:
+						try {
+							os.flush();
+							StringBuilder encryptedMessageBody = new StringBuilder();
+							String[] lines = os.toString().split("\n");
+							for (int i = 2; i < lines.length - 1; ++i) {
+								if (!lines[i].contains("Version")) {
+									encryptedMessageBody.append(lines[i].trim());
+								}
+							}
+							message.setEncryptedBody(encryptedMessageBody
+									.toString());
+							callback.success(message);
+						} catch (IOException e) {
+							callback.error(R.string.openpgp_error, message);
+						}
+
+						break;
+					case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+						callback.userInputRequried((PendingIntent) result
+								.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+								message);
+						break;
+					case OpenPgpApi.RESULT_CODE_ERROR:
+						callback.error(R.string.openpgp_error, message);
+						break;
+					}
+				}
+			});
+		} else if (message.getType() == Message.TYPE_IMAGE) {
+			try {
+				DownloadableFile inputFile = this.mXmppConnectionService
+						.getFileBackend().getFile(message, true);
+				DownloadableFile outputFile = this.mXmppConnectionService
+						.getFileBackend().getFile(message, false);
+				outputFile.createNewFile();
+				InputStream is = new FileInputStream(inputFile);
+				OutputStream os = new FileOutputStream(outputFile);
+				api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+					@Override
+					public void onReturn(Intent result) {
+						switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+								OpenPgpApi.RESULT_CODE_ERROR)) {
+						case OpenPgpApi.RESULT_CODE_SUCCESS:
+							callback.success(message);
+							break;
+						case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+							callback.userInputRequried(
+									(PendingIntent) result
+											.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+									message);
+							break;
+						case OpenPgpApi.RESULT_CODE_ERROR:
+							callback.error(R.string.openpgp_error, message);
+							break;
+						}
+					}
+				});
+			} catch (FileNotFoundException e) {
+				Log.d(Config.LOGTAG, "file not found: " + e.getMessage());
+			} catch (IOException e) {
+				Log.d(Config.LOGTAG, "io exception during file encrypt");
+			}
+		}
+	}
+
+	public long fetchKeyId(Account account, String status, String signature) {
+		if ((signature == null) || (api == null)) {
+			return 0;
+		}
+		if (status == null) {
+			status = "";
+		}
+		StringBuilder pgpSig = new StringBuilder();
+		pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----");
+		pgpSig.append('\n');
+		pgpSig.append('\n');
+		pgpSig.append(status);
+		pgpSig.append('\n');
+		pgpSig.append("-----BEGIN PGP SIGNATURE-----");
+		pgpSig.append('\n');
+		pgpSig.append('\n');
+		pgpSig.append(signature.replace("\n", "").trim());
+		pgpSig.append('\n');
+		pgpSig.append("-----END PGP SIGNATURE-----");
+		Intent params = new Intent();
+		params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+		params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+		params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+		InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
+		ByteArrayOutputStream os = new ByteArrayOutputStream();
+		Intent result = api.executeApi(params, is, os);
+		switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
+				OpenPgpApi.RESULT_CODE_ERROR)) {
+		case OpenPgpApi.RESULT_CODE_SUCCESS:
+			OpenPgpSignatureResult sigResult = result
+					.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
+			if (sigResult != null) {
+				return sigResult.getKeyId();
+			} else {
+				return 0;
+			}
+		case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+			return 0;
+		case OpenPgpApi.RESULT_CODE_ERROR:
+			Log.d(Config.LOGTAG,
+					"openpgp error: "
+							+ ((OpenPgpError) result
+									.getParcelableExtra(OpenPgpApi.RESULT_ERROR))
+									.getMessage());
+			return 0;
+		}
+		return 0;
+	}
+
+	public void generateSignature(final Account account, String status,
+			final UiCallback<Account> callback) {
+		Intent params = new Intent();
+		params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
+		params.setAction(OpenPgpApi.ACTION_SIGN);
+		params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+		InputStream is = new ByteArrayInputStream(status.getBytes());
+		final OutputStream os = new ByteArrayOutputStream();
+		api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
+
+			@Override
+			public void onReturn(Intent result) {
+				switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+				case OpenPgpApi.RESULT_CODE_SUCCESS:
+					StringBuilder signatureBuilder = new StringBuilder();
+					try {
+						os.flush();
+						String[] lines = os.toString().split("\n");
+						boolean sig = false;
+						for (String line : lines) {
+							if (sig) {
+								if (line.contains("END PGP SIGNATURE")) {
+									sig = false;
+								} else {
+									if (!line.contains("Version")) {
+										signatureBuilder.append(line.trim());
+									}
+								}
+							}
+							if (line.contains("BEGIN PGP SIGNATURE")) {
+								sig = true;
+							}
+						}
+					} catch (IOException e) {
+						callback.error(R.string.openpgp_error, account);
+						return;
+					}
+					account.setKey("pgp_signature", signatureBuilder.toString());
+					callback.success(account);
+					return;
+				case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+					callback.userInputRequried((PendingIntent) result
+							.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+							account);
+					return;
+				case OpenPgpApi.RESULT_CODE_ERROR:
+					callback.error(R.string.openpgp_error, account);
+					return;
+				}
+			}
+		});
+	}
+
+	public void hasKey(final Contact contact, final UiCallback<Contact> callback) {
+		Intent params = new Intent();
+		params.setAction(OpenPgpApi.ACTION_GET_KEY);
+		params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
+		params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount()
+				.getJid());
+		api.executeApiAsync(params, null, null, new IOpenPgpCallback() {
+
+			@Override
+			public void onReturn(Intent result) {
+				switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+				case OpenPgpApi.RESULT_CODE_SUCCESS:
+					callback.success(contact);
+					return;
+				case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+					callback.userInputRequried((PendingIntent) result
+							.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+							contact);
+					return;
+				case OpenPgpApi.RESULT_CODE_ERROR:
+					callback.error(R.string.openpgp_error, contact);
+					return;
+				}
+			}
+		});
+	}
+
+	public PendingIntent getIntentForKey(Contact contact) {
+		Intent params = new Intent();
+		params.setAction(OpenPgpApi.ACTION_GET_KEY);
+		params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
+		params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount()
+				.getJid());
+		Intent result = api.executeApi(params, null, null);
+		return (PendingIntent) result
+				.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+	}
+
+	public PendingIntent getIntentForKey(Account account, long pgpKeyId) {
+		Intent params = new Intent();
+		params.setAction(OpenPgpApi.ACTION_GET_KEY);
+		params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
+		params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid());
+		Intent result = api.executeApi(params, null, null);
+		return (PendingIntent) result
+				.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java 🔗

@@ -0,0 +1,21 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+
+public abstract class AbstractEntity {
+
+	public static final String UUID = "uuid";
+
+	protected String uuid;
+
+	public String getUuid() {
+		return this.uuid;
+	}
+
+	public abstract ContentValues getContentValues();
+
+	public boolean equals(AbstractEntity entity) {
+		return this.getUuid().equals(entity.getUuid());
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -0,0 +1,399 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.OtrEngine;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+
+public class Account extends AbstractEntity {
+
+	public static final String TABLENAME = "accounts";
+
+	public static final String USERNAME = "username";
+	public static final String SERVER = "server";
+	public static final String PASSWORD = "password";
+	public static final String OPTIONS = "options";
+	public static final String ROSTERVERSION = "rosterversion";
+	public static final String KEYS = "keys";
+	public static final String AVATAR = "avatar";
+
+	public static final int OPTION_USETLS = 0;
+	public static final int OPTION_DISABLED = 1;
+	public static final int OPTION_REGISTER = 2;
+	public static final int OPTION_USECOMPRESSION = 3;
+
+	public static final int STATUS_CONNECTING = 0;
+	public static final int STATUS_DISABLED = -2;
+	public static final int STATUS_OFFLINE = -1;
+	public static final int STATUS_ONLINE = 1;
+	public static final int STATUS_NO_INTERNET = 2;
+	public static final int STATUS_UNAUTHORIZED = 3;
+	public static final int STATUS_SERVER_NOT_FOUND = 5;
+
+	public static final int STATUS_REGISTRATION_FAILED = 7;
+	public static final int STATUS_REGISTRATION_CONFLICT = 8;
+	public static final int STATUS_REGISTRATION_SUCCESSFULL = 9;
+	public static final int STATUS_REGISTRATION_NOT_SUPPORTED = 10;
+
+	protected String username;
+	protected String server;
+	protected String password;
+	protected int options = 0;
+	protected String rosterVersion;
+	protected String resource = "mobile";
+	protected int status = -1;
+	protected JSONObject keys = new JSONObject();
+	protected String avatar;
+
+	protected boolean online = false;
+
+	private OtrEngine otrEngine = null;
+	private XmppConnection xmppConnection = null;
+	private Presences presences = new Presences();
+	private long mEndGracePeriod = 0L;
+	private String otrFingerprint;
+	private Roster roster = null;
+
+	private List<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java 🔗

@@ -0,0 +1,137 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Locale;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Bookmark extends Element implements ListItem {
+
+	private Account account;
+	private Conversation mJoinedConversation;
+
+	public Bookmark(Account account, String jid) {
+		super("conference");
+		this.setAttribute("jid", jid);
+		this.account = account;
+	}
+
+	private Bookmark(Account account) {
+		super("conference");
+		this.account = account;
+	}
+
+	public static Bookmark parse(Element element, Account account) {
+		Bookmark bookmark = new Bookmark(account);
+		bookmark.setAttributes(element.getAttributes());
+		bookmark.setChildren(element.getChildren());
+		return bookmark;
+	}
+
+	public void setAutojoin(boolean autojoin) {
+		if (autojoin) {
+			this.setAttribute("autojoin", "true");
+		} else {
+			this.setAttribute("autojoin", "false");
+		}
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public void setNick(String nick) {
+		Element element = this.findChild("nick");
+		if (element == null) {
+			element = this.addChild("nick");
+		}
+		element.setContent(nick);
+	}
+
+	public void setPassword(String password) {
+		Element element = this.findChild("password");
+		if (element != null) {
+			element.setContent(password);
+		}
+	}
+
+	@Override
+	public int compareTo(ListItem another) {
+		return this.getDisplayName().compareToIgnoreCase(
+				another.getDisplayName());
+	}
+
+	@Override
+	public String getDisplayName() {
+		if (this.mJoinedConversation != null
+				&& (this.mJoinedConversation.getMucOptions().getSubject() != null)) {
+			return this.mJoinedConversation.getMucOptions().getSubject();
+		} else if (getName() != null) {
+			return getName();
+		} else {
+			return this.getJid().split("@")[0];
+		}
+	}
+
+	@Override
+	public String getJid() {
+		String jid = this.getAttribute("jid");
+		if (jid != null) {
+			return jid.toLowerCase(Locale.US);
+		} else {
+			return null;
+		}
+	}
+
+	public String getNick() {
+		Element nick = this.findChild("nick");
+		if (nick != null) {
+			return nick.getContent();
+		} else {
+			return null;
+		}
+	}
+
+	public boolean autojoin() {
+		String autojoin = this.getAttribute("autojoin");
+		return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin
+				.equalsIgnoreCase("1")));
+	}
+
+	public String getPassword() {
+		Element password = this.findChild("password");
+		if (password != null) {
+			return password.getContent();
+		} else {
+			return null;
+		}
+	}
+
+	public boolean match(String needle) {
+		return needle == null
+				|| getJid().contains(needle.toLowerCase(Locale.US))
+				|| getDisplayName().toLowerCase(Locale.US).contains(
+						needle.toLowerCase(Locale.US));
+	}
+
+	public Account getAccount() {
+		return this.account;
+	}
+
+	public void setConversation(Conversation conversation) {
+		this.mJoinedConversation = conversation;
+	}
+
+	public Conversation getConversation() {
+		return this.mJoinedConversation;
+	}
+
+	public String getName() {
+		return this.getAttribute("name");
+	}
+
+	public void unregisterConversation() {
+		if (this.mJoinedConversation != null) {
+			this.mJoinedConversation.deregisterWithBookmark();
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -0,0 +1,367 @@
+package eu.siacs.conversations.entities;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.xml.Element;
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class Contact implements ListItem {
+	public static final String TABLENAME = "contacts";
+
+	public static final String SYSTEMNAME = "systemname";
+	public static final String SERVERNAME = "servername";
+	public static final String JID = "jid";
+	public static final String OPTIONS = "options";
+	public static final String SYSTEMACCOUNT = "systemaccount";
+	public static final String PHOTOURI = "photouri";
+	public static final String KEYS = "pgpkey";
+	public static final String ACCOUNT = "accountUuid";
+	public static final String AVATAR = "avatar";
+
+	protected String accountUuid;
+	protected String systemName;
+	protected String serverName;
+	protected String presenceName;
+	protected String jid;
+	protected int subscription = 0;
+	protected String systemAccount;
+	protected String photoUri;
+	protected String avatar;
+	protected JSONObject keys = new JSONObject();
+	protected Presences presences = new Presences();
+
+	protected Account account;
+
+	protected boolean inRoster = true;
+
+	public Lastseen lastseen = new Lastseen();
+
+	public Contact(String account, String systemName, String serverName,
+			String jid, int subscription, String photoUri,
+			String systemAccount, String keys, String avatar) {
+		this.accountUuid = account;
+		this.systemName = systemName;
+		this.serverName = serverName;
+		this.jid = jid;
+		this.subscription = subscription;
+		this.photoUri = photoUri;
+		this.systemAccount = systemAccount;
+		if (keys == null) {
+			keys = "";
+		}
+		try {
+			this.keys = new JSONObject(keys);
+		} catch (JSONException e) {
+			this.keys = new JSONObject();
+		}
+		this.avatar = avatar;
+	}
+
+	public Contact(String jid) {
+		this.jid = jid;
+	}
+
+	public String getDisplayName() {
+		if (this.systemName != null) {
+			return this.systemName;
+		} else if (this.serverName != null) {
+			return this.serverName;
+		} else if (this.presenceName != null) {
+			return this.presenceName;
+		} else {
+			return this.jid.split("@")[0];
+		}
+	}
+
+	public String getProfilePhoto() {
+		return this.photoUri;
+	}
+
+	public String getJid() {
+		return this.jid.toLowerCase(Locale.getDefault());
+	}
+
+	public boolean match(String needle) {
+		return needle == null
+				|| jid.contains(needle.toLowerCase())
+				|| getDisplayName().toLowerCase()
+						.contains(needle.toLowerCase());
+	}
+
+	public ContentValues getContentValues() {
+		ContentValues values = new ContentValues();
+		values.put(ACCOUNT, accountUuid);
+		values.put(SYSTEMNAME, systemName);
+		values.put(SERVERNAME, serverName);
+		values.put(JID, jid);
+		values.put(OPTIONS, subscription);
+		values.put(SYSTEMACCOUNT, systemAccount);
+		values.put(PHOTOURI, photoUri);
+		values.put(KEYS, keys.toString());
+		values.put(AVATAR, avatar);
+		return values;
+	}
+
+	public static Contact fromCursor(Cursor cursor) {
+		return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+				cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
+				cursor.getString(cursor.getColumnIndex(SERVERNAME)),
+				cursor.getString(cursor.getColumnIndex(JID)),
+				cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+				cursor.getString(cursor.getColumnIndex(PHOTOURI)),
+				cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
+				cursor.getString(cursor.getColumnIndex(KEYS)),
+				cursor.getString(cursor.getColumnIndex(AVATAR)));
+	}
+
+	public int getSubscription() {
+		return this.subscription;
+	}
+
+	public void setSystemAccount(String account) {
+		this.systemAccount = account;
+	}
+
+	public void setAccount(Account account) {
+		this.account = account;
+		this.accountUuid = account.getUuid();
+	}
+
+	public Account getAccount() {
+		return this.account;
+	}
+
+	public Presences getPresences() {
+		return this.presences;
+	}
+
+	public void updatePresence(String resource, int status) {
+		this.presences.updatePresence(resource, status);
+	}
+
+	public void removePresence(String resource) {
+		this.presences.removePresence(resource);
+	}
+
+	public void clearPresences() {
+		this.presences.clearPresences();
+		this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+	}
+
+	public int getMostAvailableStatus() {
+		return this.presences.getMostAvailableStatus();
+	}
+
+	public void setPresences(Presences pres) {
+		this.presences = pres;
+	}
+
+	public void setPhotoUri(String uri) {
+		this.photoUri = uri;
+	}
+
+	public void setServerName(String serverName) {
+		this.serverName = serverName;
+	}
+
+	public void setSystemName(String systemName) {
+		this.systemName = systemName;
+	}
+
+	public void setPresenceName(String presenceName) {
+		this.presenceName = presenceName;
+	}
+
+	public String getSystemAccount() {
+		return systemAccount;
+	}
+
+	public Set<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);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -0,0 +1,500 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+
+public class Conversation extends AbstractEntity {
+	public static final String TABLENAME = "conversations";
+
+	public static final int STATUS_AVAILABLE = 0;
+	public static final int STATUS_ARCHIVED = 1;
+	public static final int STATUS_DELETED = 2;
+
+	public static final int MODE_MULTI = 1;
+	public static final int MODE_SINGLE = 0;
+
+	public static final String NAME = "name";
+	public static final String ACCOUNT = "accountUuid";
+	public static final String CONTACT = "contactUuid";
+	public static final String CONTACTJID = "contactJid";
+	public static final String STATUS = "status";
+	public static final String CREATED = "created";
+	public static final String MODE = "mode";
+	public static final String ATTRIBUTES = "attributes";
+
+	public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
+	public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
+	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
+
+	private String name;
+	private String contactUuid;
+	private String accountUuid;
+	private String contactJid;
+	private int status;
+	private long created;
+	private int mode;
+
+	private JSONObject attributes = new JSONObject();
+
+	private String nextPresence;
+
+	protected ArrayList<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);
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java 🔗

@@ -0,0 +1,21 @@
+package eu.siacs.conversations.entities;
+
+public interface Downloadable {
+
+	public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" };
+	public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" };
+
+	public static final int STATUS_UNKNOWN = 0x200;
+	public static final int STATUS_CHECKING = 0x201;
+	public static final int STATUS_FAILED = 0x202;
+	public static final int STATUS_OFFER = 0x203;
+	public static final int STATUS_DOWNLOADING = 0x204;
+	public static final int STATUS_DELETED = 0x205;
+	public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+
+	public boolean start();
+
+	public int getStatus();
+
+	public long getFileSize();
+}

conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java 🔗

@@ -0,0 +1,154 @@
+package eu.siacs.conversations.entities;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
+import android.util.Log;
+
+public class DownloadableFile extends File {
+
+	private static final long serialVersionUID = 2247012619505115863L;
+
+	private long expectedSize = 0;
+	private String sha1sum;
+	private Key aeskey;
+
+	private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+			0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
+
+	public DownloadableFile(String path) {
+		super(path);
+	}
+
+	public long getSize() {
+		return super.length();
+	}
+
+	public long getExpectedSize() {
+		if (this.aeskey != null) {
+			if (this.expectedSize == 0) {
+				return 0;
+			} else {
+				return (this.expectedSize / 16 + 1) * 16;
+			}
+		} else {
+			return this.expectedSize;
+		}
+	}
+
+	public void setExpectedSize(long size) {
+		this.expectedSize = size;
+	}
+
+	public String getSha1Sum() {
+		return this.sha1sum;
+	}
+
+	public void setSha1Sum(String sum) {
+		this.sha1sum = sum;
+	}
+
+	public void setKey(byte[] key) {
+		if (key.length == 48) {
+			byte[] secretKey = new byte[32];
+			byte[] iv = new byte[16];
+			System.arraycopy(key, 0, iv, 0, 16);
+			System.arraycopy(key, 16, secretKey, 0, 32);
+			this.aeskey = new SecretKeySpec(secretKey, "AES");
+			this.iv = iv;
+		} else if (key.length >= 32) {
+			byte[] secretKey = new byte[32];
+			System.arraycopy(key, 0, secretKey, 0, 32);
+			this.aeskey = new SecretKeySpec(secretKey, "AES");
+		} else if (key.length >= 16) {
+			byte[] secretKey = new byte[16];
+			System.arraycopy(key, 0, secretKey, 0, 16);
+			this.aeskey = new SecretKeySpec(secretKey, "AES");
+		}
+	}
+
+	public Key getKey() {
+		return this.aeskey;
+	}
+
+	public InputStream createInputStream() {
+		if (this.getKey() == null) {
+			try {
+				return new FileInputStream(this);
+			} catch (FileNotFoundException e) {
+				return null;
+			}
+		} else {
+			try {
+				IvParameterSpec ips = new IvParameterSpec(iv);
+				Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+				cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips);
+				Log.d(Config.LOGTAG, "opening encrypted input stream");
+				return new CipherInputStream(new FileInputStream(this), cipher);
+			} catch (NoSuchAlgorithmException e) {
+				Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
+				return null;
+			} catch (NoSuchPaddingException e) {
+				Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
+				return null;
+			} catch (InvalidKeyException e) {
+				Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
+				return null;
+			} catch (InvalidAlgorithmParameterException e) {
+				Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
+				return null;
+			} catch (FileNotFoundException e) {
+				return null;
+			}
+		}
+	}
+
+	public OutputStream createOutputStream() {
+		if (this.getKey() == null) {
+			try {
+				return new FileOutputStream(this);
+			} catch (FileNotFoundException e) {
+				return null;
+			}
+		} else {
+			try {
+				IvParameterSpec ips = new IvParameterSpec(this.iv);
+				Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+				cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips);
+				Log.d(Config.LOGTAG, "opening encrypted output stream");
+				return new CipherOutputStream(new FileOutputStream(this),
+						cipher);
+			} catch (NoSuchAlgorithmException e) {
+				Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
+				return null;
+			} catch (NoSuchPaddingException e) {
+				Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
+				return null;
+			} catch (InvalidKeyException e) {
+				Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
+				return null;
+			} catch (InvalidAlgorithmParameterException e) {
+				Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
+				return null;
+			} catch (FileNotFoundException e) {
+				return null;
+			}
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/Message.java 🔗

@@ -0,0 +1,478 @@
+package eu.siacs.conversations.entities;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+
+public class Message extends AbstractEntity {
+
+	public static final String TABLENAME = "messages";
+
+	public static final int STATUS_RECEIVED = 0;
+	public static final int STATUS_UNSEND = 1;
+	public static final int STATUS_SEND = 2;
+	public static final int STATUS_SEND_FAILED = 3;
+	public static final int STATUS_SEND_REJECTED = 4;
+	public static final int STATUS_WAITING = 5;
+	public static final int STATUS_OFFERED = 6;
+	public static final int STATUS_SEND_RECEIVED = 7;
+	public static final int STATUS_SEND_DISPLAYED = 8;
+
+	public static final int ENCRYPTION_NONE = 0;
+	public static final int ENCRYPTION_PGP = 1;
+	public static final int ENCRYPTION_OTR = 2;
+	public static final int ENCRYPTION_DECRYPTED = 3;
+	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
+
+	public static final int TYPE_TEXT = 0;
+	public static final int TYPE_IMAGE = 1;
+	public static final int TYPE_AUDIO = 2;
+	public static final int TYPE_STATUS = 3;
+	public static final int TYPE_PRIVATE = 4;
+
+	public static String CONVERSATION = "conversationUuid";
+	public static String COUNTERPART = "counterpart";
+	public static String TRUE_COUNTERPART = "trueCounterpart";
+	public static String BODY = "body";
+	public static String TIME_SENT = "timeSent";
+	public static String ENCRYPTION = "encryption";
+	public static String STATUS = "status";
+	public static String TYPE = "type";
+	public static String REMOTE_MSG_ID = "remoteMsgId";
+
+	protected String conversationUuid;
+	protected String counterpart;
+	protected String trueCounterpart;
+	protected String body;
+	protected String encryptedBody;
+	protected long timeSent;
+	protected int encryption;
+	protected int status;
+	protected int type;
+	protected boolean read = true;
+	protected String remoteMsgId = null;
+
+	protected Conversation conversation = null;
+	protected Downloadable downloadable = null;
+	public boolean markable = false;
+
+	private Message mNextMessage = null;
+	private Message mPreviousMessage = null;
+
+	private Message() {
+
+	}
+
+	public Message(Conversation conversation, String body, int encryption) {
+		this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
+				conversation.getContactJid(), null, body, System
+						.currentTimeMillis(), encryption,
+				Message.STATUS_UNSEND, TYPE_TEXT, null);
+		this.conversation = conversation;
+	}
+
+	public Message(Conversation conversation, String counterpart, String body,
+			int encryption, int status) {
+		this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
+				counterpart, null, body, System.currentTimeMillis(),
+				encryption, status, TYPE_TEXT, null);
+		this.conversation = conversation;
+	}
+
+	public Message(String uuid, String conversationUUid, String counterpart,
+			String trueCounterpart, String body, long timeSent, int encryption,
+			int status, int type, String remoteMsgId) {
+		this.uuid = uuid;
+		this.conversationUuid = conversationUUid;
+		this.counterpart = counterpart;
+		this.trueCounterpart = trueCounterpart;
+		this.body = body;
+		this.timeSent = timeSent;
+		this.encryption = encryption;
+		this.status = status;
+		this.type = type;
+		this.remoteMsgId = remoteMsgId;
+	}
+
+	@Override
+	public ContentValues getContentValues() {
+		ContentValues values = new ContentValues();
+		values.put(UUID, uuid);
+		values.put(CONVERSATION, conversationUuid);
+		values.put(COUNTERPART, counterpart);
+		values.put(TRUE_COUNTERPART, trueCounterpart);
+		values.put(BODY, body);
+		values.put(TIME_SENT, timeSent);
+		values.put(ENCRYPTION, encryption);
+		values.put(STATUS, status);
+		values.put(TYPE, type);
+		values.put(REMOTE_MSG_ID, remoteMsgId);
+		return values;
+	}
+
+	public String getConversationUuid() {
+		return conversationUuid;
+	}
+
+	public Conversation getConversation() {
+		return this.conversation;
+	}
+
+	public String getCounterpart() {
+		return counterpart;
+	}
+
+	public Contact getContact() {
+		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
+			return this.conversation.getContact();
+		} else {
+			if (this.trueCounterpart == null) {
+				return null;
+			} else {
+				return this.conversation.getAccount().getRoster()
+						.getContactFromRoster(this.trueCounterpart);
+			}
+		}
+	}
+
+	public String getBody() {
+		return body;
+	}
+
+	public String getReadableBody(Context context) {
+		if (encryption == ENCRYPTION_PGP) {
+			return context.getText(R.string.encrypted_message_received)
+					.toString();
+		} else if (encryption == ENCRYPTION_DECRYPTION_FAILED) {
+			return context.getText(R.string.decryption_failed).toString();
+		} else if (type == TYPE_IMAGE) {
+			return context.getText(R.string.image_file).toString();
+		} else {
+			return body.trim();
+		}
+	}
+
+	public long getTimeSent() {
+		return timeSent;
+	}
+
+	public int getEncryption() {
+		return encryption;
+	}
+
+	public int getStatus() {
+		return status;
+	}
+
+	public String getRemoteMsgId() {
+		return this.remoteMsgId;
+	}
+
+	public void setRemoteMsgId(String id) {
+		this.remoteMsgId = id;
+	}
+
+	public static Message fromCursor(Cursor cursor) {
+		return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
+				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
+				cursor.getString(cursor.getColumnIndex(COUNTERPART)),
+				cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)),
+				cursor.getString(cursor.getColumnIndex(BODY)),
+				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
+				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
+				cursor.getInt(cursor.getColumnIndex(STATUS)),
+				cursor.getInt(cursor.getColumnIndex(TYPE)),
+				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)));
+	}
+
+	public void setConversation(Conversation conv) {
+		this.conversation = conv;
+	}
+
+	public void setStatus(int status) {
+		this.status = status;
+	}
+
+	public boolean isRead() {
+		return this.read;
+	}
+
+	public void markRead() {
+		this.read = true;
+	}
+
+	public void markUnread() {
+		this.read = false;
+	}
+
+	public void setTime(long time) {
+		this.timeSent = time;
+	}
+
+	public void setEncryption(int encryption) {
+		this.encryption = encryption;
+	}
+
+	public void setBody(String body) {
+		this.body = body;
+	}
+
+	public String getEncryptedBody() {
+		return this.encryptedBody;
+	}
+
+	public void setEncryptedBody(String body) {
+		this.encryptedBody = body;
+	}
+
+	public void setType(int type) {
+		this.type = type;
+	}
+
+	public int getType() {
+		return this.type;
+	}
+
+	public void setPresence(String presence) {
+		if (presence == null) {
+			this.counterpart = this.counterpart.split("/", 2)[0];
+		} else {
+			this.counterpart = this.counterpart.split("/", 2)[0] + "/"
+					+ presence;
+		}
+	}
+
+	public void setTrueCounterpart(String trueCounterpart) {
+		this.trueCounterpart = trueCounterpart;
+	}
+
+	public String getPresence() {
+		String[] counterparts = this.counterpart.split("/", 2);
+		if (counterparts.length == 2) {
+			return counterparts[1];
+		} else {
+			if (this.counterpart.contains("/")) {
+				return "";
+			} else {
+				return null;
+			}
+		}
+	}
+
+	public void setDownloadable(Downloadable downloadable) {
+		this.downloadable = downloadable;
+	}
+
+	public Downloadable getDownloadable() {
+		return this.downloadable;
+	}
+
+	public static Message createStatusMessage(Conversation conversation) {
+		Message message = new Message();
+		message.setType(Message.TYPE_STATUS);
+		message.setConversation(conversation);
+		return message;
+	}
+
+	public void setCounterpart(String counterpart) {
+		this.counterpart = counterpart;
+	}
+
+	public boolean equals(Message message) {
+		if ((this.remoteMsgId != null) && (this.body != null)
+				&& (this.counterpart != null)) {
+			return this.remoteMsgId.equals(message.getRemoteMsgId())
+					&& this.body.equals(message.getBody())
+					&& this.counterpart.equals(message.getCounterpart());
+		} else {
+			return false;
+		}
+	}
+
+	public Message next() {
+		if (this.mNextMessage == null) {
+			synchronized (this.conversation.messages) {
+				int index = this.conversation.messages.indexOf(this);
+				if (index < 0
+						|| index >= this.conversation.getMessages().size() - 1) {
+					this.mNextMessage = null;
+				} else {
+					this.mNextMessage = this.conversation.messages
+							.get(index + 1);
+				}
+			}
+		}
+		return this.mNextMessage;
+	}
+
+	public Message prev() {
+		if (this.mPreviousMessage == null) {
+			synchronized (this.conversation.messages) {
+				int index = this.conversation.messages.indexOf(this);
+				if (index <= 0 || index > this.conversation.messages.size()) {
+					this.mPreviousMessage = null;
+				} else {
+					this.mPreviousMessage = this.conversation.messages
+							.get(index - 1);
+				}
+			}
+		}
+		return this.mPreviousMessage;
+	}
+
+	public boolean mergable(Message message) {
+		if (message == null) {
+			return false;
+		}
+		return (message.getType() == Message.TYPE_TEXT
+				&& this.getDownloadable() == null
+				&& message.getDownloadable() == null
+				&& message.getEncryption() != Message.ENCRYPTION_PGP
+				&& this.getType() == message.getType()
+				&& this.getEncryption() == message.getEncryption()
+				&& this.getCounterpart().equals(message.getCounterpart())
+				&& (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && ((this
+				.getStatus() == message.getStatus() || ((this.getStatus() == Message.STATUS_SEND || this
+				.getStatus() == Message.STATUS_SEND_RECEIVED) && (message
+				.getStatus() == Message.STATUS_UNSEND
+				|| message.getStatus() == Message.STATUS_SEND || message
+					.getStatus() == Message.STATUS_SEND_DISPLAYED)))));
+	}
+
+	public String getMergedBody() {
+		Message next = this.next();
+		if (this.mergable(next)) {
+			return body.trim() + '\n' + next.getMergedBody();
+		}
+		return body.trim();
+	}
+
+	public int getMergedStatus() {
+		Message next = this.next();
+		if (this.mergable(next)) {
+			return next.getMergedStatus();
+		} else {
+			return getStatus();
+		}
+	}
+
+	public long getMergedTimeSent() {
+		Message next = this.next();
+		if (this.mergable(next)) {
+			return next.getMergedTimeSent();
+		} else {
+			return getTimeSent();
+		}
+	}
+
+	public boolean wasMergedIntoPrevious() {
+		Message prev = this.prev();
+		if (prev == null) {
+			return false;
+		} else {
+			return prev.mergable(this);
+		}
+	}
+
+	public boolean bodyContainsDownloadable() {
+		Contact contact = this.getContact();
+		if (status <= STATUS_RECEIVED
+				&& (contact == null || !contact.trusted())) {
+			return false;
+		}
+		try {
+			URL url = new URL(this.getBody());
+			if (!url.getProtocol().equalsIgnoreCase("http")
+					&& !url.getProtocol().equalsIgnoreCase("https")) {
+				return false;
+			}
+			if (url.getPath() == null) {
+				return false;
+			}
+			String[] pathParts = url.getPath().split("/");
+			String filename = pathParts[pathParts.length - 1];
+			String[] extensionParts = filename.split("\\.");
+			if (extensionParts.length == 2
+					&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+							extensionParts[extensionParts.length - 1])) {
+				return true;
+			} else if (extensionParts.length == 3
+					&& Arrays
+							.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
+							.contains(extensionParts[extensionParts.length - 1])
+					&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+							extensionParts[extensionParts.length - 2])) {
+				return true;
+			} else {
+				return false;
+			}
+		} catch (MalformedURLException e) {
+			return false;
+		}
+	}
+
+	public ImageParams getImageParams() {
+		ImageParams params = new ImageParams();
+		if (this.downloadable != null) {
+			params.size = this.downloadable.getFileSize();
+		}
+		if (body == null) {
+			return params;
+		}
+		String parts[] = body.split(",");
+		if (parts.length == 1) {
+			try {
+				params.size = Long.parseLong(parts[0]);
+			} catch (NumberFormatException e) {
+				params.origin = parts[0];
+			}
+		} else if (parts.length == 3) {
+			try {
+				params.size = Long.parseLong(parts[0]);
+			} catch (NumberFormatException e) {
+				params.size = 0;
+			}
+			try {
+				params.width = Integer.parseInt(parts[1]);
+			} catch (NumberFormatException e) {
+				params.width = 0;
+			}
+			try {
+				params.height = Integer.parseInt(parts[2]);
+			} catch (NumberFormatException e) {
+				params.height = 0;
+			}
+		} else if (parts.length == 4) {
+			params.origin = parts[0];
+			try {
+				params.size = Long.parseLong(parts[1]);
+			} catch (NumberFormatException e) {
+				params.size = 0;
+			}
+			try {
+				params.width = Integer.parseInt(parts[2]);
+			} catch (NumberFormatException e) {
+				params.width = 0;
+			}
+			try {
+				params.height = Integer.parseInt(parts[3]);
+			} catch (NumberFormatException e) {
+				params.height = 0;
+			}
+		}
+		return params;
+	}
+
+	public class ImageParams {
+		public long size = 0;
+		public int width = 0;
+		public int height = 0;
+		public String origin;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java 🔗

@@ -0,0 +1,369 @@
+package eu.siacs.conversations.entities;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import android.annotation.SuppressLint;
+
+@SuppressLint("DefaultLocale")
+public class MucOptions {
+	public static final int ERROR_NO_ERROR = 0;
+	public static final int ERROR_NICK_IN_USE = 1;
+	public static final int ERROR_ROOM_NOT_FOUND = 2;
+	public static final int ERROR_PASSWORD_REQUIRED = 3;
+	public static final int ERROR_BANNED = 4;
+	public static final int ERROR_MEMBERS_ONLY = 5;
+
+	public static final int KICKED_FROM_ROOM = 9;
+
+	public static final String STATUS_CODE_BANNED = "301";
+	public static final String STATUS_CODE_KICKED = "307";
+
+	public interface OnRenameListener {
+		public void onRename(boolean success);
+	}
+
+	public class User {
+		public static final int ROLE_MODERATOR = 3;
+		public static final int ROLE_NONE = 0;
+		public static final int ROLE_PARTICIPANT = 2;
+		public static final int ROLE_VISITOR = 1;
+		public static final int AFFILIATION_ADMIN = 4;
+		public static final int AFFILIATION_OWNER = 3;
+		public static final int AFFILIATION_MEMBER = 2;
+		public static final int AFFILIATION_OUTCAST = 1;
+		public static final int AFFILIATION_NONE = 0;
+
+		private int role;
+		private int affiliation;
+		private String name;
+		private String jid;
+		private long pgpKeyId = 0;
+
+		public String getName() {
+			return name;
+		}
+
+		public void setName(String user) {
+			this.name = user;
+		}
+
+		public void setJid(String jid) {
+			this.jid = jid;
+		}
+
+		public String getJid() {
+			return this.jid;
+		}
+
+		public int getRole() {
+			return this.role;
+		}
+
+		public void setRole(String role) {
+			role = role.toLowerCase();
+			if (role.equals("moderator")) {
+				this.role = ROLE_MODERATOR;
+			} else if (role.equals("participant")) {
+				this.role = ROLE_PARTICIPANT;
+			} else if (role.equals("visitor")) {
+				this.role = ROLE_VISITOR;
+			} else {
+				this.role = ROLE_NONE;
+			}
+		}
+
+		public int getAffiliation() {
+			return this.affiliation;
+		}
+
+		public void setAffiliation(String affiliation) {
+			if (affiliation.equalsIgnoreCase("admin")) {
+				this.affiliation = AFFILIATION_ADMIN;
+			} else if (affiliation.equalsIgnoreCase("owner")) {
+				this.affiliation = AFFILIATION_OWNER;
+			} else if (affiliation.equalsIgnoreCase("member")) {
+				this.affiliation = AFFILIATION_MEMBER;
+			} else if (affiliation.equalsIgnoreCase("outcast")) {
+				this.affiliation = AFFILIATION_OUTCAST;
+			} else {
+				this.affiliation = AFFILIATION_NONE;
+			}
+		}
+
+		public void setPgpKeyId(long id) {
+			this.pgpKeyId = id;
+		}
+
+		public long getPgpKeyId() {
+			return this.pgpKeyId;
+		}
+
+		public Contact getContact() {
+			return account.getRoster().getContactFromRoster(getJid());
+		}
+	}
+
+	private Account account;
+	private List<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/Presences.java 🔗

@@ -0,0 +1,76 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Presences {
+
+	public static final int CHAT = -1;
+	public static final int ONLINE = 0;
+	public static final int AWAY = 1;
+	public static final int XA = 2;
+	public static final int DND = 3;
+	public static final int OFFLINE = 4;
+
+	private Hashtable<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);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/entities/Roster.java 🔗

@@ -0,0 +1,83 @@
+package eu.siacs.conversations.entities;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class Roster {
+	Account account;
+	ConcurrentHashMap<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java 🔗

@@ -0,0 +1,48 @@
+package eu.siacs.conversations.generator;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+
+import android.util.Base64;
+
+public abstract class AbstractGenerator {
+	public final String[] FEATURES = { "urn:xmpp:jingle:1",
+			"urn:xmpp:jingle:apps:file-transfer:3",
+			"urn:xmpp:jingle:transports:s5b:1",
+			"urn:xmpp:jingle:transports:ibb:1", "urn:xmpp:receipts",
+			"urn:xmpp:chat-markers:0", "http://jabber.org/protocol/muc",
+			"jabber:x:conference", "http://jabber.org/protocol/caps",
+			"http://jabber.org/protocol/disco#info",
+			"urn:xmpp:avatar:metadata+notify" };
+	public final String IDENTITY_NAME = "Conversations 0.7";
+	public final String IDENTITY_TYPE = "phone";
+
+	protected XmppConnectionService mXmppConnectionService;
+
+	protected AbstractGenerator(XmppConnectionService service) {
+		this.mXmppConnectionService = service;
+	}
+
+	public String getCapHash() {
+		StringBuilder s = new StringBuilder();
+		s.append("client/" + IDENTITY_TYPE + "//" + IDENTITY_NAME + "<");
+		MessageDigest md = null;
+		try {
+			md = MessageDigest.getInstance("SHA-1");
+		} catch (NoSuchAlgorithmException e) {
+			return null;
+		}
+		List<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();
+	}
+}

conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -0,0 +1,96 @@
+package eu.siacs.conversations.generator;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqGenerator extends AbstractGenerator {
+
+	public IqGenerator(XmppConnectionService service) {
+		super(service);
+	}
+
+	public IqPacket discoResponse(IqPacket request) {
+		IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT);
+		packet.setId(request.getId());
+		packet.setTo(request.getFrom());
+		Element query = packet.addChild("query",
+				"http://jabber.org/protocol/disco#info");
+		query.setAttribute("node", request.query().getAttribute("node"));
+		Element identity = query.addChild("identity");
+		identity.setAttribute("category", "client");
+		identity.setAttribute("type", this.IDENTITY_TYPE);
+		identity.setAttribute("name", IDENTITY_NAME);
+		List<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -0,0 +1,178 @@
+package eu.siacs.conversations.generator;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageGenerator extends AbstractGenerator {
+	public MessageGenerator(XmppConnectionService service) {
+		super(service);
+	}
+
+	private MessagePacket preparePacket(Message message, boolean addDelay) {
+		Conversation conversation = message.getConversation();
+		Account account = conversation.getAccount();
+		MessagePacket packet = new MessagePacket();
+		if (conversation.getMode() == Conversation.MODE_SINGLE) {
+			packet.setTo(message.getCounterpart());
+			packet.setType(MessagePacket.TYPE_CHAT);
+			packet.addChild("markable", "urn:xmpp:chat-markers:0");
+			if (this.mXmppConnectionService.indicateReceived()) {
+				packet.addChild("request", "urn:xmpp:receipts");
+			}
+		} else if (message.getType() == Message.TYPE_PRIVATE) {
+			packet.setTo(message.getCounterpart());
+			packet.setType(MessagePacket.TYPE_CHAT);
+		} else {
+			packet.setTo(message.getCounterpart().split("/", 2)[0]);
+			packet.setType(MessagePacket.TYPE_GROUPCHAT);
+		}
+		packet.setFrom(account.getFullJid());
+		packet.setId(message.getUuid());
+		if (addDelay) {
+			addDelay(packet, message.getTimeSent());
+		}
+		return packet;
+	}
+
+	private void addDelay(MessagePacket packet, long timestamp) {
+		final SimpleDateFormat mDateFormat = new SimpleDateFormat(
+				"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+		mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+		Element delay = packet.addChild("delay", "urn:xmpp:delay");
+		Date date = new Date(timestamp);
+		delay.setAttribute("stamp", mDateFormat.format(date));
+	}
+
+	public MessagePacket generateOtrChat(Message message) {
+		return generateOtrChat(message, false);
+	}
+
+	public MessagePacket generateOtrChat(Message message, boolean addDelay) {
+		Session otrSession = message.getConversation().getOtrSession();
+		if (otrSession == null) {
+			return null;
+		}
+		MessagePacket packet = preparePacket(message, addDelay);
+		packet.addChild("private", "urn:xmpp:carbons:2");
+		packet.addChild("no-copy", "urn:xmpp:hints");
+		try {
+			packet.setBody(otrSession.transformSending(message.getBody()));
+			return packet;
+		} catch (OtrException e) {
+			return null;
+		}
+	}
+
+	public MessagePacket generateChat(Message message) {
+		return generateChat(message, false);
+	}
+
+	public MessagePacket generateChat(Message message, boolean addDelay) {
+		MessagePacket packet = preparePacket(message, addDelay);
+		packet.setBody(message.getBody());
+		return packet;
+	}
+
+	public MessagePacket generatePgpChat(Message message) {
+		return generatePgpChat(message, false);
+	}
+
+	public MessagePacket generatePgpChat(Message message, boolean addDelay) {
+		MessagePacket packet = preparePacket(message, addDelay);
+		packet.setBody("This is an XEP-0027 encryted message");
+		if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+			packet.addChild("x", "jabber:x:encrypted").setContent(
+					message.getEncryptedBody());
+		} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+			packet.addChild("x", "jabber:x:encrypted").setContent(
+					message.getBody());
+		}
+		return packet;
+	}
+
+	public MessagePacket generateNotAcceptable(MessagePacket origin) {
+		MessagePacket packet = generateError(origin);
+		Element error = packet.addChild("error");
+		error.setAttribute("type", "modify");
+		error.setAttribute("code", "406");
+		error.addChild("not-acceptable");
+		return packet;
+	}
+
+	private MessagePacket generateError(MessagePacket origin) {
+		MessagePacket packet = new MessagePacket();
+		packet.setId(origin.getId());
+		packet.setTo(origin.getFrom());
+		packet.setBody(origin.getBody());
+		packet.setType(MessagePacket.TYPE_ERROR);
+		return packet;
+	}
+
+	public MessagePacket confirm(Account account, String to, String id) {
+		MessagePacket packet = new MessagePacket();
+		packet.setType(MessagePacket.TYPE_NORMAL);
+		packet.setTo(to);
+		packet.setFrom(account.getFullJid());
+		Element received = packet.addChild("displayed",
+				"urn:xmpp:chat-markers:0");
+		received.setAttribute("id", id);
+		return packet;
+	}
+
+	public MessagePacket conferenceSubject(Conversation conversation,
+			String subject) {
+		MessagePacket packet = new MessagePacket();
+		packet.setType(MessagePacket.TYPE_GROUPCHAT);
+		packet.setTo(conversation.getContactJid().split("/", 2)[0]);
+		Element subjectChild = new Element("subject");
+		subjectChild.setContent(subject);
+		packet.addChild(subjectChild);
+		packet.setFrom(conversation.getAccount().getJid());
+		return packet;
+	}
+
+	public MessagePacket directInvite(Conversation conversation, String contact) {
+		MessagePacket packet = new MessagePacket();
+		packet.setType(MessagePacket.TYPE_NORMAL);
+		packet.setTo(contact);
+		packet.setFrom(conversation.getAccount().getFullJid());
+		Element x = packet.addChild("x", "jabber:x:conference");
+		x.setAttribute("jid", conversation.getContactJid().split("/", 2)[0]);
+		return packet;
+	}
+
+	public MessagePacket invite(Conversation conversation, String contact) {
+		MessagePacket packet = new MessagePacket();
+		packet.setTo(conversation.getContactJid().split("/", 2)[0]);
+		packet.setFrom(conversation.getAccount().getFullJid());
+		Element x = new Element("x");
+		x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
+		Element invite = new Element("invite");
+		invite.setAttribute("to", contact);
+		x.addChild(invite);
+		packet.addChild(x);
+		return packet;
+	}
+
+	public MessagePacket received(Account account,
+			MessagePacket originalMessage, String namespace) {
+		MessagePacket receivedPacket = new MessagePacket();
+		receivedPacket.setType(MessagePacket.TYPE_NORMAL);
+		receivedPacket.setTo(originalMessage.getFrom());
+		receivedPacket.setFrom(account.getFullJid());
+		Element received = receivedPacket.addChild("received", namespace);
+		received.setAttribute("id", originalMessage.getId());
+		return receivedPacket;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java 🔗

@@ -0,0 +1,57 @@
+package eu.siacs.conversations.generator;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public class PresenceGenerator extends AbstractGenerator {
+
+	public PresenceGenerator(XmppConnectionService service) {
+		super(service);
+	}
+
+	private PresencePacket subscription(String type, Contact contact) {
+		PresencePacket packet = new PresencePacket();
+		packet.setAttribute("type", type);
+		packet.setAttribute("to", contact.getJid());
+		packet.setAttribute("from", contact.getAccount().getJid());
+		return packet;
+	}
+
+	public PresencePacket requestPresenceUpdatesFrom(Contact contact) {
+		return subscription("subscribe", contact);
+	}
+
+	public PresencePacket stopPresenceUpdatesFrom(Contact contact) {
+		return subscription("unsubscribe", contact);
+	}
+
+	public PresencePacket stopPresenceUpdatesTo(Contact contact) {
+		return subscription("unsubscribed", contact);
+	}
+
+	public PresencePacket sendPresenceUpdatesTo(Contact contact) {
+		return subscription("subscribed", contact);
+	}
+
+	public PresencePacket sendPresence(Account account) {
+		PresencePacket packet = new PresencePacket();
+		packet.setAttribute("from", account.getFullJid());
+		String sig = account.getPgpSignature();
+		if (sig != null) {
+			packet.addChild("status").setContent("online");
+			packet.addChild("x", "jabber:x:signed").setContent(sig);
+		}
+		String capHash = getCapHash();
+		if (capHash != null) {
+			Element cap = packet.addChild("c",
+					"http://jabber.org/protocol/caps");
+			cap.setAttribute("hash", "sha-1");
+			cap.setAttribute("node", "http://conversions.siacs.eu");
+			cap.setAttribute("ver", capHash);
+		}
+		return packet;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java 🔗

@@ -0,0 +1,255 @@
+package eu.siacs.conversations.http;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class HttpConnection implements Downloadable {
+
+	private HttpConnectionManager mHttpConnectionManager;
+	private XmppConnectionService mXmppConnectionService;
+
+	private URL mUrl;
+	private Message message;
+	private DownloadableFile file;
+	private int mStatus = Downloadable.STATUS_UNKNOWN;
+
+	public HttpConnection(HttpConnectionManager manager) {
+		this.mHttpConnectionManager = manager;
+		this.mXmppConnectionService = manager.getXmppConnectionService();
+	}
+
+	@Override
+	public boolean start() {
+		if (mXmppConnectionService.hasInternetConnection()) {
+			if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
+				checkFileSize(true);
+			} else {
+				new Thread(new FileDownloader(true)).start();
+			}
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	public void init(Message message) {
+		this.message = message;
+		this.message.setDownloadable(this);
+		try {
+			mUrl = new URL(message.getBody());
+			this.file = mXmppConnectionService.getFileBackend().getFile(
+					message, false);
+			String reference = mUrl.getRef();
+			if (reference != null && reference.length() == 96) {
+				this.file.setKey(CryptoHelper.hexToBytes(reference));
+			}
+			if (this.message.getEncryption() == Message.ENCRYPTION_OTR
+					&& this.file.getKey() == null) {
+				this.message.setEncryption(Message.ENCRYPTION_NONE);
+			}
+			checkFileSize(false);
+		} catch (MalformedURLException e) {
+			this.cancel();
+		}
+	}
+
+	private void checkFileSize(boolean interactive) {
+		new Thread(new FileSizeChecker(interactive)).start();
+	}
+
+	public void cancel() {
+		mHttpConnectionManager.finishConnection(this);
+		message.setDownloadable(null);
+		mXmppConnectionService.updateConversationUi();
+	}
+
+	private void finish() {
+		Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+		intent.setData(Uri.fromFile(file));
+		mXmppConnectionService.sendBroadcast(intent);
+		message.setDownloadable(null);
+		mHttpConnectionManager.finishConnection(this);
+	}
+
+	private void changeStatus(int status) {
+		this.mStatus = status;
+		mXmppConnectionService.updateConversationUi();
+	}
+
+	private void setupTrustManager(HttpsURLConnection connection,
+			boolean interactive) {
+		X509TrustManager trustManager;
+		HostnameVerifier hostnameVerifier;
+		if (interactive) {
+			trustManager = mXmppConnectionService.getMemorizingTrustManager();
+			hostnameVerifier = mXmppConnectionService
+					.getMemorizingTrustManager().wrapHostnameVerifier(
+							new StrictHostnameVerifier());
+		} else {
+			trustManager = mXmppConnectionService.getMemorizingTrustManager()
+					.getNonInteractive();
+			hostnameVerifier = mXmppConnectionService
+					.getMemorizingTrustManager()
+					.wrapHostnameVerifierNonInteractive(
+							new StrictHostnameVerifier());
+		}
+		try {
+			SSLContext sc = SSLContext.getInstance("TLS");
+			sc.init(null, new X509TrustManager[] { trustManager },
+					mXmppConnectionService.getRNG());
+			connection.setSSLSocketFactory(sc.getSocketFactory());
+			connection.setHostnameVerifier(hostnameVerifier);
+		} catch (KeyManagementException e) {
+			return;
+		} catch (NoSuchAlgorithmException e) {
+			return;
+		}
+	}
+
+	private class FileSizeChecker implements Runnable {
+
+		private boolean interactive = false;
+
+		public FileSizeChecker(boolean interactive) {
+			this.interactive = interactive;
+		}
+
+		@Override
+		public void run() {
+			long size;
+			try {
+				size = retrieveFileSize();
+			} catch (SSLHandshakeException e) {
+				changeStatus(STATUS_OFFER_CHECK_FILESIZE);
+				return;
+			} catch (IOException e) {
+				cancel();
+				return;
+			}
+			file.setExpectedSize(size);
+			if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
+				new Thread(new FileDownloader(interactive)).start();
+			} else {
+				changeStatus(STATUS_OFFER);
+			}
+		}
+
+		private long retrieveFileSize() throws IOException,
+				SSLHandshakeException {
+			changeStatus(STATUS_CHECKING);
+			HttpURLConnection connection = (HttpURLConnection) mUrl
+					.openConnection();
+			connection.setRequestMethod("HEAD");
+			if (connection instanceof HttpsURLConnection) {
+				setupTrustManager((HttpsURLConnection) connection, interactive);
+			}
+			connection.connect();
+			String contentLength = connection.getHeaderField("Content-Length");
+			if (contentLength == null) {
+				throw new IOException();
+			}
+			try {
+				return Long.parseLong(contentLength, 10);
+			} catch (NumberFormatException e) {
+				throw new IOException();
+			}
+		}
+
+	}
+
+	private class FileDownloader implements Runnable {
+
+		private boolean interactive = false;
+
+		public FileDownloader(boolean interactive) {
+			this.interactive = interactive;
+		}
+
+		@Override
+		public void run() {
+			try {
+				changeStatus(STATUS_DOWNLOADING);
+				download();
+				updateImageBounds();
+				finish();
+			} catch (SSLHandshakeException e) {
+				changeStatus(STATUS_OFFER);
+			} catch (IOException e) {
+				cancel();
+			}
+		}
+
+		private void download() throws SSLHandshakeException, IOException {
+			HttpURLConnection connection = (HttpURLConnection) mUrl
+					.openConnection();
+			if (connection instanceof HttpsURLConnection) {
+				setupTrustManager((HttpsURLConnection) connection, interactive);
+			}
+			connection.connect();
+			BufferedInputStream is = new BufferedInputStream(
+					connection.getInputStream());
+			OutputStream os = file.createOutputStream();
+			int count = -1;
+			byte[] buffer = new byte[1024];
+			while ((count = is.read(buffer)) != -1) {
+				os.write(buffer, 0, count);
+			}
+			os.flush();
+			os.close();
+			is.close();
+		}
+
+		private void updateImageBounds() {
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			options.inJustDecodeBounds = true;
+			BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+			int imageHeight = options.outHeight;
+			int imageWidth = options.outWidth;
+			message.setBody(mUrl.toString() + "," + file.getSize() + ','
+					+ imageWidth + ',' + imageHeight);
+			message.setType(Message.TYPE_IMAGE);
+			mXmppConnectionService.updateMessage(message);
+		}
+
+	}
+
+	@Override
+	public int getStatus() {
+		return this.mStatus;
+	}
+
+	@Override
+	public long getFileSize() {
+		if (this.file != null) {
+			return this.file.getExpectedSize();
+		} else {
+			return 0;
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java 🔗

@@ -0,0 +1,28 @@
+package eu.siacs.conversations.http;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public class HttpConnectionManager extends AbstractConnectionManager {
+
+	public HttpConnectionManager(XmppConnectionService service) {
+		super(service);
+	}
+
+	private List<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);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java 🔗

@@ -0,0 +1,92 @@
+package eu.siacs.conversations.parser;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+
+public abstract class AbstractParser {
+
+	protected XmppConnectionService mXmppConnectionService;
+
+	protected AbstractParser(XmppConnectionService service) {
+		this.mXmppConnectionService = service;
+	}
+
+	protected long getTimestamp(Element packet) {
+		long now = System.currentTimeMillis();
+		ArrayList<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();
+	}
+}

conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java 🔗

@@ -0,0 +1,92 @@
+package eu.siacs.conversations.parser;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class IqParser extends AbstractParser implements OnIqPacketReceived {
+
+	public IqParser(XmppConnectionService service) {
+		super(service);
+	}
+
+	public void rosterItems(Account account, Element query) {
+		String version = query.getAttribute("ver");
+		if (version != null) {
+			account.getRoster().setVersion(version);
+		}
+		for (Element item : query.getChildren()) {
+			if (item.getName().equals("item")) {
+				String jid = item.getAttribute("jid");
+				String name = item.getAttribute("name");
+				String subscription = item.getAttribute("subscription");
+				Contact contact = account.getRoster().getContact(jid);
+				if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
+					contact.setServerName(name);
+				}
+				if (subscription != null) {
+					if (subscription.equals("remove")) {
+						contact.resetOption(Contact.Options.IN_ROSTER);
+						contact.resetOption(Contact.Options.DIRTY_DELETE);
+						contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+					} else {
+						contact.setOption(Contact.Options.IN_ROSTER);
+						contact.resetOption(Contact.Options.DIRTY_PUSH);
+						contact.parseSubscriptionFromElement(item);
+					}
+				}
+			}
+		}
+		mXmppConnectionService.updateRosterUi();
+	}
+
+	public String avatarData(IqPacket packet) {
+		Element pubsub = packet.findChild("pubsub",
+				"http://jabber.org/protocol/pubsub");
+		if (pubsub == null) {
+			return null;
+		}
+		Element items = pubsub.findChild("items");
+		if (items == null) {
+			return null;
+		}
+		return super.avatarData(items);
+	}
+
+	@Override
+	public void onIqPacketReceived(Account account, IqPacket packet) {
+		if (packet.hasChild("query", "jabber:iq:roster")) {
+			String from = packet.getFrom();
+			if ((from == null) || (from.equals(account.getJid()))) {
+				Element query = packet.findChild("query");
+				this.rosterItems(account, query);
+			}
+		} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
+				|| packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
+			mXmppConnectionService.getJingleConnectionManager()
+					.deliverIbbPacket(account, packet);
+		} else if (packet.hasChild("query",
+				"http://jabber.org/protocol/disco#info")) {
+			IqPacket response = mXmppConnectionService.getIqGenerator()
+					.discoResponse(packet);
+			account.getXmppConnection().sendIqPacket(response, null);
+		} else if (packet.hasChild("ping", "urn:xmpp:ping")) {
+			IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
+			mXmppConnectionService.sendIqPacket(account, response, null);
+		} else {
+			if ((packet.getType() == IqPacket.TYPE_GET)
+					|| (packet.getType() == IqPacket.TYPE_SET)) {
+				IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR);
+				Element error = response.addChild("error");
+				error.setAttribute("type", "cancel");
+				error.addChild("feature-not-implemented",
+						"urn:ietf:params:xml:ns:xmpp-stanzas");
+				account.getXmppConnection().sendIqPacket(response, null);
+			}
+		}
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -0,0 +1,517 @@
+package eu.siacs.conversations.parser;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.NotificationService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class MessageParser extends AbstractParser implements
+		OnMessagePacketReceived {
+	public MessageParser(XmppConnectionService service) {
+		super(service);
+	}
+
+	private Message parseChat(MessagePacket packet, Account account) {
+		String[] fromParts = packet.getFrom().split("/", 2);
+		Conversation conversation = mXmppConnectionService
+				.findOrCreateConversation(account, fromParts[0], false);
+		updateLastseen(packet, account, true);
+		String pgpBody = getPgpBody(packet);
+		Message finishedMessage;
+		if (pgpBody != null) {
+			finishedMessage = new Message(conversation, packet.getFrom(),
+					pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED);
+		} else {
+			finishedMessage = new Message(conversation, packet.getFrom(),
+					packet.getBody(), Message.ENCRYPTION_NONE,
+					Message.STATUS_RECEIVED);
+		}
+		finishedMessage.setRemoteMsgId(packet.getId());
+		finishedMessage.markable = isMarkable(packet);
+		if (conversation.getMode() == Conversation.MODE_MULTI
+				&& fromParts.length >= 2) {
+			finishedMessage.setType(Message.TYPE_PRIVATE);
+			finishedMessage.setPresence(fromParts[1]);
+			finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+					.getTrueCounterpart(fromParts[1]));
+			if (conversation.hasDuplicateMessage(finishedMessage)) {
+				return null;
+			}
+
+		}
+		finishedMessage.setTime(getTimestamp(packet));
+		return finishedMessage;
+	}
+
+	private Message parseOtrChat(MessagePacket packet, Account account) {
+		boolean properlyAddressed = (packet.getTo().split("/", 2).length == 2)
+				|| (account.countPresences() == 1);
+		String[] fromParts = packet.getFrom().split("/", 2);
+		Conversation conversation = mXmppConnectionService
+				.findOrCreateConversation(account, fromParts[0], false);
+		String presence;
+		if (fromParts.length >= 2) {
+			presence = fromParts[1];
+		} else {
+			presence = "";
+		}
+		updateLastseen(packet, account, true);
+		String body = packet.getBody();
+		if (body.matches("^\\?OTRv\\d*\\?")) {
+			conversation.endOtrIfNeeded();
+		}
+		if (!conversation.hasValidOtrSession()) {
+			if (properlyAddressed) {
+				conversation.startOtrSession(mXmppConnectionService, presence,
+						false);
+			} else {
+				return null;
+			}
+		} else {
+			String foreignPresence = conversation.getOtrSession()
+					.getSessionID().getUserID();
+			if (!foreignPresence.equals(presence)) {
+				conversation.endOtrIfNeeded();
+				if (properlyAddressed) {
+					conversation.startOtrSession(mXmppConnectionService,
+							presence, false);
+				} else {
+					return null;
+				}
+			}
+		}
+		try {
+			Session otrSession = conversation.getOtrSession();
+			SessionStatus before = otrSession.getSessionStatus();
+			body = otrSession.transformReceiving(body);
+			SessionStatus after = otrSession.getSessionStatus();
+			if ((before != after) && (after == SessionStatus.ENCRYPTED)) {
+				mXmppConnectionService.onOtrSessionEstablished(conversation);
+			} else if ((before != after) && (after == SessionStatus.FINISHED)) {
+				conversation.resetOtrSession();
+				mXmppConnectionService.updateConversationUi();
+			}
+			if ((body == null) || (body.isEmpty())) {
+				return null;
+			}
+			if (body.startsWith(CryptoHelper.FILETRANSFER)) {
+				String key = body.substring(CryptoHelper.FILETRANSFER.length());
+				conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
+				return null;
+			}
+			Message finishedMessage = new Message(conversation,
+					packet.getFrom(), body, Message.ENCRYPTION_OTR,
+					Message.STATUS_RECEIVED);
+			finishedMessage.setTime(getTimestamp(packet));
+			finishedMessage.setRemoteMsgId(packet.getId());
+			finishedMessage.markable = isMarkable(packet);
+			return finishedMessage;
+		} catch (Exception e) {
+			String receivedId = packet.getId();
+			if (receivedId != null) {
+				mXmppConnectionService.replyWithNotAcceptable(account, packet);
+			}
+			conversation.resetOtrSession();
+			return null;
+		}
+	}
+
+	private Message parseGroupchat(MessagePacket packet, Account account) {
+		int status;
+		String[] fromParts = packet.getFrom().split("/", 2);
+		if (mXmppConnectionService.find(account.pendingConferenceLeaves,
+				account, fromParts[0]) != null) {
+			return null;
+		}
+		Conversation conversation = mXmppConnectionService
+				.findOrCreateConversation(account, fromParts[0], true);
+		if (packet.hasChild("subject")) {
+			conversation.getMucOptions().setSubject(
+					packet.findChild("subject").getContent());
+			mXmppConnectionService.updateConversationUi();
+			return null;
+		}
+		if ((fromParts.length == 1)) {
+			return null;
+		}
+		String counterPart = fromParts[1];
+		if (counterPart.equals(conversation.getMucOptions().getActualNick())) {
+			if (mXmppConnectionService.markMessage(conversation,
+					packet.getId(), Message.STATUS_SEND)) {
+				return null;
+			} else {
+				status = Message.STATUS_SEND;
+			}
+		} else {
+			status = Message.STATUS_RECEIVED;
+		}
+		String pgpBody = getPgpBody(packet);
+		Message finishedMessage;
+		if (pgpBody == null) {
+			finishedMessage = new Message(conversation, counterPart,
+					packet.getBody(), Message.ENCRYPTION_NONE, status);
+		} else {
+			finishedMessage = new Message(conversation, counterPart, pgpBody,
+					Message.ENCRYPTION_PGP, status);
+		}
+		finishedMessage.setRemoteMsgId(packet.getId());
+		finishedMessage.markable = isMarkable(packet);
+		if (status == Message.STATUS_RECEIVED) {
+			finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+					.getTrueCounterpart(counterPart));
+		}
+		if (packet.hasChild("delay")
+				&& conversation.hasDuplicateMessage(finishedMessage)) {
+			return null;
+		}
+		finishedMessage.setTime(getTimestamp(packet));
+		return finishedMessage;
+	}
+
+	private Message parseCarbonMessage(MessagePacket packet, Account account) {
+		int status;
+		String fullJid;
+		Element forwarded;
+		if (packet.hasChild("received", "urn:xmpp:carbons:2")) {
+			forwarded = packet.findChild("received", "urn:xmpp:carbons:2")
+					.findChild("forwarded", "urn:xmpp:forward:0");
+			status = Message.STATUS_RECEIVED;
+		} else if (packet.hasChild("sent", "urn:xmpp:carbons:2")) {
+			forwarded = packet.findChild("sent", "urn:xmpp:carbons:2")
+					.findChild("forwarded", "urn:xmpp:forward:0");
+			status = Message.STATUS_SEND;
+		} else {
+			return null;
+		}
+		if (forwarded == null) {
+			return null;
+		}
+		Element message = forwarded.findChild("message");
+		if (message == null) {
+			return null;
+		}
+		if (!message.hasChild("body")) {
+			if (status == Message.STATUS_RECEIVED
+					&& message.getAttribute("from") != null) {
+				parseNonMessage(message, account);
+			} else if (status == Message.STATUS_SEND
+					&& message.hasChild("displayed", "urn:xmpp:chat-markers:0")) {
+				String to = message.getAttribute("to");
+				if (to != null) {
+					Conversation conversation = mXmppConnectionService.find(
+							mXmppConnectionService.getConversations(), account,
+							to.split("/")[0]);
+					if (conversation != null) {
+						mXmppConnectionService.markRead(conversation, false);
+					}
+				}
+			}
+			return null;
+		}
+		if (status == Message.STATUS_RECEIVED) {
+			fullJid = message.getAttribute("from");
+			if (fullJid == null) {
+				return null;
+			} else {
+				updateLastseen(message, account, true);
+			}
+		} else {
+			fullJid = message.getAttribute("to");
+			if (fullJid == null) {
+				return null;
+			}
+		}
+		String[] parts = fullJid.split("/", 2);
+		Conversation conversation = mXmppConnectionService
+				.findOrCreateConversation(account, parts[0], false);
+		String pgpBody = getPgpBody(message);
+		Message finishedMessage;
+		if (pgpBody != null) {
+			finishedMessage = new Message(conversation, fullJid, pgpBody,
+					Message.ENCRYPTION_PGP, status);
+		} else {
+			String body = message.findChild("body").getContent();
+			finishedMessage = new Message(conversation, fullJid, body,
+					Message.ENCRYPTION_NONE, status);
+		}
+		finishedMessage.setTime(getTimestamp(message));
+		finishedMessage.setRemoteMsgId(message.getAttribute("id"));
+		finishedMessage.markable = isMarkable(message);
+		if (conversation.getMode() == Conversation.MODE_MULTI
+				&& parts.length >= 2) {
+			finishedMessage.setType(Message.TYPE_PRIVATE);
+			finishedMessage.setPresence(parts[1]);
+			finishedMessage.setTrueCounterpart(conversation.getMucOptions()
+					.getTrueCounterpart(parts[1]));
+			if (conversation.hasDuplicateMessage(finishedMessage)) {
+				return null;
+			}
+		}
+
+		return finishedMessage;
+	}
+
+	private void parseError(MessagePacket packet, Account account) {
+		String[] fromParts = packet.getFrom().split("/", 2);
+		mXmppConnectionService.markMessage(account, fromParts[0],
+				packet.getId(), Message.STATUS_SEND_FAILED);
+	}
+
+	private void parseNonMessage(Element packet, Account account) {
+		String from = packet.getAttribute("from");
+		if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) {
+			Element event = packet.findChild("event",
+					"http://jabber.org/protocol/pubsub#event");
+			parseEvent(event, packet.getAttribute("from"), account);
+		} else if (from != null
+				&& packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) {
+			String id = packet
+					.findChild("displayed", "urn:xmpp:chat-markers:0")
+					.getAttribute("id");
+			updateLastseen(packet, account, true);
+			mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+					id, Message.STATUS_SEND_DISPLAYED);
+		} else if (from != null
+				&& packet.hasChild("received", "urn:xmpp:chat-markers:0")) {
+			String id = packet.findChild("received", "urn:xmpp:chat-markers:0")
+					.getAttribute("id");
+			updateLastseen(packet, account, false);
+			mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+					id, Message.STATUS_SEND_RECEIVED);
+		} else if (from != null
+				&& packet.hasChild("received", "urn:xmpp:receipts")) {
+			String id = packet.findChild("received", "urn:xmpp:receipts")
+					.getAttribute("id");
+			updateLastseen(packet, account, false);
+			mXmppConnectionService.markMessage(account, from.split("/", 2)[0],
+					id, Message.STATUS_SEND_RECEIVED);
+		} else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+			Element x = packet.findChild("x",
+					"http://jabber.org/protocol/muc#user");
+			if (x.hasChild("invite")) {
+				Conversation conversation = mXmppConnectionService
+						.findOrCreateConversation(account,
+								packet.getAttribute("from"), true);
+				if (!conversation.getMucOptions().online()) {
+					if (x.hasChild("password")) {
+						Element password = x.findChild("password");
+						conversation.getMucOptions().setPassword(
+								password.getContent());
+						mXmppConnectionService.databaseBackend
+								.updateConversation(conversation);
+					}
+					mXmppConnectionService.joinMuc(conversation);
+					mXmppConnectionService.updateConversationUi();
+				}
+			}
+		} else if (packet.hasChild("x", "jabber:x:conference")) {
+			Element x = packet.findChild("x", "jabber:x:conference");
+			String jid = x.getAttribute("jid");
+			String password = x.getAttribute("password");
+			if (jid != null) {
+				Conversation conversation = mXmppConnectionService
+						.findOrCreateConversation(account, jid, true);
+				if (!conversation.getMucOptions().online()) {
+					if (password != null) {
+						conversation.getMucOptions().setPassword(password);
+						mXmppConnectionService.databaseBackend
+								.updateConversation(conversation);
+					}
+					mXmppConnectionService.joinMuc(conversation);
+					mXmppConnectionService.updateConversationUi();
+				}
+			}
+		}
+	}
+
+	private void parseEvent(Element event, String from, Account account) {
+		Element items = event.findChild("items");
+		String node = items.getAttribute("node");
+		if (node != null) {
+			if (node.equals("urn:xmpp:avatar:metadata")) {
+				Avatar avatar = Avatar.parseMetadata(items);
+				if (avatar != null) {
+					avatar.owner = from;
+					if (mXmppConnectionService.getFileBackend().isAvatarCached(
+							avatar)) {
+						if (account.getJid().equals(from)) {
+							if (account.setAvatar(avatar.getFilename())) {
+								mXmppConnectionService.databaseBackend
+										.updateAccount(account);
+							}
+							mXmppConnectionService.getAvatarService().clear(
+									account);
+							mXmppConnectionService.updateConversationUi();
+							mXmppConnectionService.updateAccountUi();
+						} else {
+							Contact contact = account.getRoster().getContact(
+									from);
+							contact.setAvatar(avatar.getFilename());
+							mXmppConnectionService.getAvatarService().clear(
+									contact);
+							mXmppConnectionService.updateConversationUi();
+							mXmppConnectionService.updateRosterUi();
+						}
+					} else {
+						mXmppConnectionService.fetchAvatar(account, avatar);
+					}
+				}
+			} else if (node.equals("http://jabber.org/protocol/nick")) {
+				Element item = items.findChild("item");
+				if (item != null) {
+					Element nick = item.findChild("nick",
+							"http://jabber.org/protocol/nick");
+					if (nick != null) {
+						if (from != null) {
+							Contact contact = account.getRoster().getContact(
+									from);
+							contact.setPresenceName(nick.getContent());
+						}
+					}
+				}
+			}
+		}
+	}
+
+	private String getPgpBody(Element message) {
+		Element child = message.findChild("x", "jabber:x:encrypted");
+		if (child == null) {
+			return null;
+		} else {
+			return child.getContent();
+		}
+	}
+
+	private boolean isMarkable(Element message) {
+		return message.hasChild("markable", "urn:xmpp:chat-markers:0");
+	}
+
+	@Override
+	public void onMessagePacketReceived(Account account, MessagePacket packet) {
+		Message message = null;
+		boolean notify = mXmppConnectionService.getPreferences().getBoolean(
+				"show_notification", true);
+		boolean alwaysNotifyInConference = notify
+				&& mXmppConnectionService.getPreferences().getBoolean(
+						"always_notify_in_conference", false);
+
+		this.parseNick(packet, account);
+
+		if ((packet.getType() == MessagePacket.TYPE_CHAT || packet.getType() == MessagePacket.TYPE_NORMAL)) {
+			if ((packet.getBody() != null)
+					&& (packet.getBody().startsWith("?OTR"))) {
+				message = this.parseOtrChat(packet, account);
+				if (message != null) {
+					message.markUnread();
+				}
+			} else if (packet.hasChild("body")
+					&& !(packet.hasChild("x",
+							"http://jabber.org/protocol/muc#user"))) {
+				message = this.parseChat(packet, account);
+				if (message != null) {
+					message.markUnread();
+				}
+			} else if (packet.hasChild("received", "urn:xmpp:carbons:2")
+					|| (packet.hasChild("sent", "urn:xmpp:carbons:2"))) {
+				message = this.parseCarbonMessage(packet, account);
+				if (message != null) {
+					if (message.getStatus() == Message.STATUS_SEND) {
+						account.activateGracePeriod();
+						notify = false;
+						mXmppConnectionService.markRead(
+								message.getConversation(), false);
+					} else {
+						message.markUnread();
+					}
+				}
+			} else {
+				parseNonMessage(packet, account);
+			}
+		} else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
+			message = this.parseGroupchat(packet, account);
+			if (message != null) {
+				if (message.getStatus() == Message.STATUS_RECEIVED) {
+					message.markUnread();
+					notify = alwaysNotifyInConference
+							|| NotificationService
+									.wasHighlightedOrPrivate(message);
+				} else {
+					mXmppConnectionService.markRead(message.getConversation(),
+							false);
+					account.activateGracePeriod();
+					notify = false;
+				}
+			}
+		} else if (packet.getType() == MessagePacket.TYPE_ERROR) {
+			this.parseError(packet, account);
+			return;
+		} else if (packet.getType() == MessagePacket.TYPE_HEADLINE) {
+			this.parseHeadline(packet, account);
+			return;
+		}
+		if ((message == null) || (message.getBody() == null)) {
+			return;
+		}
+		if ((mXmppConnectionService.confirmMessages())
+				&& ((packet.getId() != null))) {
+			if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
+				MessagePacket receipt = mXmppConnectionService
+						.getMessageGenerator().received(account, packet,
+								"urn:xmpp:chat-markers:0");
+				mXmppConnectionService.sendMessagePacket(account, receipt);
+			}
+			if (packet.hasChild("request", "urn:xmpp:receipts")) {
+				MessagePacket receipt = mXmppConnectionService
+						.getMessageGenerator().received(account, packet,
+								"urn:xmpp:receipts");
+				mXmppConnectionService.sendMessagePacket(account, receipt);
+			}
+		}
+		Conversation conversation = message.getConversation();
+		conversation.add(message);
+		if (packet.getType() != MessagePacket.TYPE_ERROR) {
+			if (message.getEncryption() == Message.ENCRYPTION_NONE
+					|| mXmppConnectionService.saveEncryptedMessages()) {
+				mXmppConnectionService.databaseBackend.createMessage(message);
+			}
+		}
+		if (message.bodyContainsDownloadable()) {
+			this.mXmppConnectionService.getHttpConnectionManager()
+					.createNewConnection(message);
+		}
+		notify = notify && !conversation.isMuted();
+		if (notify) {
+			mXmppConnectionService.getNotificationService().push(message);
+		}
+		mXmppConnectionService.updateConversationUi();
+	}
+
+	private void parseHeadline(MessagePacket packet, Account account) {
+		if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) {
+			Element event = packet.findChild("event",
+					"http://jabber.org/protocol/pubsub#event");
+			parseEvent(event, packet.getFrom(), account);
+		}
+	}
+
+	private void parseNick(MessagePacket packet, Account account) {
+		Element nick = packet.findChild("nick",
+				"http://jabber.org/protocol/nick");
+		if (nick != null) {
+			if (packet.getFrom() != null) {
+				Contact contact = account.getRoster().getContact(
+						packet.getFrom());
+				contact.setPresenceName(nick.getContent());
+			}
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java 🔗

@@ -0,0 +1,133 @@
+package eu.siacs.conversations.parser;
+
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public class PresenceParser extends AbstractParser implements
+		OnPresencePacketReceived {
+
+	public PresenceParser(XmppConnectionService service) {
+		super(service);
+	}
+
+	public void parseConferencePresence(PresencePacket packet, Account account) {
+		PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine();
+		if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+			Conversation muc = mXmppConnectionService.find(account, packet
+					.getAttribute("from").split("/", 2)[0]);
+			if (muc != null) {
+				boolean before = muc.getMucOptions().online();
+				muc.getMucOptions().processPacket(packet, mPgpEngine);
+				if (before != muc.getMucOptions().online()) {
+					mXmppConnectionService.updateConversationUi();
+				}
+				mXmppConnectionService.getAvatarService().clear(muc);
+			}
+		} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
+			Conversation muc = mXmppConnectionService.find(account, packet
+					.getAttribute("from").split("/", 2)[0]);
+			if (muc != null) {
+				boolean before = muc.getMucOptions().online();
+				muc.getMucOptions().processPacket(packet, mPgpEngine);
+				if (before != muc.getMucOptions().online()) {
+					mXmppConnectionService.updateConversationUi();
+				}
+				mXmppConnectionService.getAvatarService().clear(muc);
+			}
+		}
+	}
+
+	public void parseContactPresence(PresencePacket packet, Account account) {
+		PresenceGenerator mPresenceGenerator = mXmppConnectionService
+				.getPresenceGenerator();
+		if (packet.getFrom() == null) {
+			return;
+		}
+		String[] fromParts = packet.getFrom().split("/", 2);
+		String type = packet.getAttribute("type");
+		if (fromParts[0].equals(account.getJid())) {
+			if (fromParts.length == 2) {
+				if (type == null) {
+					account.updatePresence(fromParts[1],
+							Presences.parseShow(packet.findChild("show")));
+				} else if (type.equals("unavailable")) {
+					account.removePresence(fromParts[1]);
+					account.deactivateGracePeriod();
+				}
+			}
+		} else {
+			Contact contact = account.getRoster().getContact(packet.getFrom());
+			if (type == null) {
+				String presence;
+				if (fromParts.length >= 2) {
+					presence = fromParts[1];
+				} else {
+					presence = "";
+				}
+				int sizeBefore = contact.getPresences().size();
+				contact.updatePresence(presence,
+						Presences.parseShow(packet.findChild("show")));
+				PgpEngine pgp = mXmppConnectionService.getPgpEngine();
+				if (pgp != null) {
+					Element x = packet.findChild("x", "jabber:x:signed");
+					if (x != null) {
+						Element status = packet.findChild("status");
+						String msg;
+						if (status != null) {
+							msg = status.getContent();
+						} else {
+							msg = "";
+						}
+						contact.setPgpKeyId(pgp.fetchKeyId(account, msg,
+								x.getContent()));
+					}
+				}
+				boolean online = sizeBefore < contact.getPresences().size();
+				updateLastseen(packet, account, true);
+				mXmppConnectionService.onContactStatusChanged
+						.onContactStatusChanged(contact, online);
+			} else if (type.equals("unavailable")) {
+				if (fromParts.length != 2) {
+					contact.clearPresences();
+				} else {
+					contact.removePresence(fromParts[1]);
+				}
+				mXmppConnectionService.onContactStatusChanged
+						.onContactStatusChanged(contact, false);
+			} else if (type.equals("subscribe")) {
+				if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
+					mXmppConnectionService.sendPresencePacket(account,
+							mPresenceGenerator.sendPresenceUpdatesTo(contact));
+				} else {
+					contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
+				}
+			}
+			Element nick = packet.findChild("nick",
+					"http://jabber.org/protocol/nick");
+			if (nick != null) {
+				contact.setPresenceName(nick.getContent());
+			}
+		}
+		mXmppConnectionService.updateRosterUi();
+	}
+
+	@Override
+	public void onPresencePacketReceived(Account account, PresencePacket packet) {
+		if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
+			this.parseConferencePresence(packet, account);
+		} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
+			this.parseConferencePresence(packet, account);
+		} else {
+			this.parseContactPresence(packet, account);
+		}
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -0,0 +1,335 @@
+package eu.siacs.conversations.persistance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Roster;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class DatabaseBackend extends SQLiteOpenHelper {
+
+	private static DatabaseBackend instance = null;
+
+	private static final String DATABASE_NAME = "history";
+	private static final int DATABASE_VERSION = 8;
+
+	private static String CREATE_CONTATCS_STATEMENT = "create table "
+			+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
+			+ Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT,"
+			+ Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
+			+ Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
+			+ Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
+			+ "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
+			+ Account.TABLENAME + "(" + Account.UUID
+			+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+			+ Contact.JID + ") ON CONFLICT REPLACE);";
+
+	private DatabaseBackend(Context context) {
+		super(context, DATABASE_NAME, null, DATABASE_VERSION);
+	}
+
+	@Override
+	public void onCreate(SQLiteDatabase db) {
+		db.execSQL("PRAGMA foreign_keys=ON;");
+		db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID
+				+ " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT,"
+				+ Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT,"
+				+ Account.ROSTERVERSION + " TEXT," + Account.OPTIONS
+				+ " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS
+				+ " TEXT)");
+		db.execSQL("create table " + Conversation.TABLENAME + " ("
+				+ Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
+				+ " TEXT, " + Conversation.CONTACT + " TEXT, "
+				+ Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID
+				+ " TEXT, " + Conversation.CREATED + " NUMBER, "
+				+ Conversation.STATUS + " NUMBER, " + Conversation.MODE
+				+ " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY("
+				+ Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME
+				+ "(" + Account.UUID + ") ON DELETE CASCADE);");
+		db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID
+				+ " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, "
+				+ Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART
+				+ " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
+				+ Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
+				+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+				+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+				+ Message.CONVERSATION + ") REFERENCES "
+				+ Conversation.TABLENAME + "(" + Conversation.UUID
+				+ ") ON DELETE CASCADE);");
+
+		db.execSQL(CREATE_CONTATCS_STATEMENT);
+	}
+
+	@Override
+	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+		if (oldVersion < 2 && newVersion >= 2) {
+			db.execSQL("update " + Account.TABLENAME + " set "
+					+ Account.OPTIONS + " = " + Account.OPTIONS + " | 8");
+		}
+		if (oldVersion < 3 && newVersion >= 3) {
+			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+					+ Message.TYPE + " NUMBER");
+		}
+		if (oldVersion < 5 && newVersion >= 5) {
+			db.execSQL("DROP TABLE " + Contact.TABLENAME);
+			db.execSQL(CREATE_CONTATCS_STATEMENT);
+			db.execSQL("UPDATE " + Account.TABLENAME + " SET "
+					+ Account.ROSTERVERSION + " = NULL");
+		}
+		if (oldVersion < 6 && newVersion >= 6) {
+			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+					+ Message.TRUE_COUNTERPART + " TEXT");
+		}
+		if (oldVersion < 7 && newVersion >= 7) {
+			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+					+ Message.REMOTE_MSG_ID + " TEXT");
+			db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+					+ Contact.AVATAR + " TEXT");
+			db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN "
+					+ Account.AVATAR + " TEXT");
+		}
+		if (oldVersion < 8 && newVersion >= 8) {
+			db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
+					+ Conversation.ATTRIBUTES + " TEXT");
+		}
+	}
+
+	public static synchronized DatabaseBackend getInstance(Context context) {
+		if (instance == null) {
+			instance = new DatabaseBackend(context);
+		}
+		return instance;
+	}
+
+	public void createConversation(Conversation conversation) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
+	}
+
+	public void createMessage(Message message) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		db.insert(Message.TABLENAME, null, message.getContentValues());
+	}
+
+	public void createAccount(Account account) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		db.insert(Account.TABLENAME, null, account.getContentValues());
+	}
+
+	public void createContact(Contact contact) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		db.insert(Contact.TABLENAME, null, contact.getContentValues());
+	}
+
+	public int getConversationCount() {
+		SQLiteDatabase db = this.getReadableDatabase();
+		Cursor cursor = db.rawQuery("select count(uuid) as count from "
+				+ Conversation.TABLENAME + " where " + Conversation.STATUS
+				+ "=" + Conversation.STATUS_AVAILABLE, null);
+		cursor.moveToFirst();
+		return cursor.getInt(0);
+	}
+
+	public CopyOnWriteArrayList<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);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -0,0 +1,480 @@
+package eu.siacs.conversations.persistance;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class FileBackend {
+
+	private static int IMAGE_SIZE = 1920;
+
+	private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
+			"yyyyMMdd_HHmmssSSS", Locale.US);
+
+	private XmppConnectionService mXmppConnectionService;
+
+	public FileBackend(XmppConnectionService service) {
+		this.mXmppConnectionService = service;
+	}
+
+	public DownloadableFile getFile(Message message) {
+		return getFile(message, true);
+	}
+
+	public DownloadableFile getFile(Message message, boolean decrypted) {
+		StringBuilder filename = new StringBuilder();
+		filename.append(getConversationsDirectory());
+		filename.append(message.getUuid());
+		if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
+			filename.append(".webp");
+		} else {
+			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+				filename.append(".webp");
+			} else {
+				filename.append(".webp.pgp");
+			}
+		}
+		return new DownloadableFile(filename.toString());
+	}
+
+	public static String getConversationsDirectory() {
+		return Environment.getExternalStoragePublicDirectory(
+				Environment.DIRECTORY_PICTURES).getAbsolutePath()
+				+ "/Conversations/";
+	}
+
+	public Bitmap resize(Bitmap originalBitmap, int size) {
+		int w = originalBitmap.getWidth();
+		int h = originalBitmap.getHeight();
+		if (Math.max(w, h) > size) {
+			int scalledW;
+			int scalledH;
+			if (w <= h) {
+				scalledW = (int) (w / ((double) h / size));
+				scalledH = size;
+			} else {
+				scalledW = size;
+				scalledH = (int) (h / ((double) w / size));
+			}
+			Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap,
+					scalledW, scalledH, true);
+			return scalledBitmap;
+		} else {
+			return originalBitmap;
+		}
+	}
+
+	public Bitmap rotate(Bitmap bitmap, int degree) {
+		int w = bitmap.getWidth();
+		int h = bitmap.getHeight();
+		Matrix mtx = new Matrix();
+		mtx.postRotate(degree);
+		return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
+	}
+
+	public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
+			throws ImageCopyException {
+		return this.copyImageToPrivateStorage(message, image, 0);
+	}
+
+	private DownloadableFile copyImageToPrivateStorage(Message message,
+			Uri image, int sampleSize) throws ImageCopyException {
+		try {
+			InputStream is = mXmppConnectionService.getContentResolver()
+					.openInputStream(image);
+			DownloadableFile file = getFile(message);
+			file.getParentFile().mkdirs();
+			file.createNewFile();
+			Bitmap originalBitmap;
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			int inSampleSize = (int) Math.pow(2, sampleSize);
+			Log.d(Config.LOGTAG, "reading bitmap with sample size "
+					+ inSampleSize);
+			options.inSampleSize = inSampleSize;
+			originalBitmap = BitmapFactory.decodeStream(is, null, options);
+			is.close();
+			if (originalBitmap == null) {
+				throw new ImageCopyException(R.string.error_not_an_image_file);
+			}
+			Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
+			originalBitmap = null;
+			int rotation = getRotation(image);
+			if (rotation > 0) {
+				scalledBitmap = rotate(scalledBitmap, rotation);
+			}
+			OutputStream os = new FileOutputStream(file);
+			boolean success = scalledBitmap.compress(
+					Bitmap.CompressFormat.WEBP, 75, os);
+			if (!success) {
+				throw new ImageCopyException(R.string.error_compressing_image);
+			}
+			os.flush();
+			os.close();
+			long size = file.getSize();
+			int width = scalledBitmap.getWidth();
+			int height = scalledBitmap.getHeight();
+			message.setBody(Long.toString(size) + ',' + width + ',' + height);
+			return file;
+		} catch (FileNotFoundException e) {
+			throw new ImageCopyException(R.string.error_file_not_found);
+		} catch (IOException e) {
+			throw new ImageCopyException(R.string.error_io_exception);
+		} catch (SecurityException e) {
+			throw new ImageCopyException(
+					R.string.error_security_exception_during_image_copy);
+		} catch (OutOfMemoryError e) {
+			++sampleSize;
+			if (sampleSize <= 3) {
+				return copyImageToPrivateStorage(message, image, sampleSize);
+			} else {
+				throw new ImageCopyException(R.string.error_out_of_memory);
+			}
+		}
+	}
+
+	private int getRotation(Uri image) {
+		if ("content".equals(image.getScheme())) {
+			try {
+				Cursor cursor = mXmppConnectionService
+						.getContentResolver()
+						.query(image,
+								new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
+								null, null, null);
+				if (cursor.getCount() != 1) {
+					return -1;
+				}
+				cursor.moveToFirst();
+				return cursor.getInt(0);
+			} catch (IllegalArgumentException e) {
+				return -1;
+			}
+		} else {
+			ExifInterface exif;
+			try {
+				exif = new ExifInterface(image.toString());
+				if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+						.equalsIgnoreCase("6")) {
+					return 90;
+				} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+						.equalsIgnoreCase("8")) {
+					return 270;
+				} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
+						.equalsIgnoreCase("3")) {
+					return 180;
+				} else {
+					return 0;
+				}
+			} catch (IOException e) {
+				return -1;
+			}
+		}
+	}
+
+	public Bitmap getImageFromMessage(Message message) {
+		return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
+	}
+
+	public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
+			throws FileNotFoundException {
+		Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
+				message.getUuid());
+		if ((thumbnail == null) && (!cacheOnly)) {
+			File file = getFile(message);
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			options.inSampleSize = calcSampleSize(file, size);
+			Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
+					options);
+			if (fullsize == null) {
+				throw new FileNotFoundException();
+			}
+			thumbnail = resize(fullsize, size);
+			this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
+					thumbnail);
+		}
+		return thumbnail;
+	}
+
+	public void removeFiles(Conversation conversation) {
+		String prefix = mXmppConnectionService.getFilesDir().getAbsolutePath();
+		String path = prefix + "/" + conversation.getAccount().getJid() + "/"
+				+ conversation.getContactJid();
+		File file = new File(path);
+		try {
+			this.deleteFile(file);
+		} catch (IOException e) {
+			Log.d(Config.LOGTAG,
+					"error deleting file: " + file.getAbsolutePath());
+		}
+	}
+
+	private void deleteFile(File f) throws IOException {
+		if (f.isDirectory()) {
+			for (File c : f.listFiles())
+				deleteFile(c);
+		}
+		f.delete();
+	}
+
+	public Uri getTakePhotoUri() {
+		StringBuilder pathBuilder = new StringBuilder();
+		pathBuilder.append(Environment
+				.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+		pathBuilder.append('/');
+		pathBuilder.append("Camera");
+		pathBuilder.append('/');
+		pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date())
+				+ ".jpg");
+		Uri uri = Uri.parse("file://" + pathBuilder.toString());
+		File file = new File(uri.toString());
+		file.getParentFile().mkdirs();
+		return uri;
+	}
+
+	public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+		try {
+			Avatar avatar = new Avatar();
+			Bitmap bm = cropCenterSquare(image, size);
+			if (bm == null) {
+				return null;
+			}
+			ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+			Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
+					mByteArrayOutputStream, Base64.DEFAULT);
+			MessageDigest digest = MessageDigest.getInstance("SHA-1");
+			DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+					mBase64OutputSttream, digest);
+			if (!bm.compress(format, 75, mDigestOutputStream)) {
+				return null;
+			}
+			mDigestOutputStream.flush();
+			mDigestOutputStream.close();
+			avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+			avatar.image = new String(mByteArrayOutputStream.toByteArray());
+			return avatar;
+		} catch (NoSuchAlgorithmException e) {
+			return null;
+		} catch (IOException e) {
+			return null;
+		}
+	}
+
+	public boolean isAvatarCached(Avatar avatar) {
+		File file = new File(getAvatarPath(avatar.getFilename()));
+		return file.exists();
+	}
+
+	public boolean save(Avatar avatar) {
+		if (isAvatarCached(avatar)) {
+			return true;
+		}
+		String filename = getAvatarPath(avatar.getFilename());
+		File file = new File(filename + ".tmp");
+		file.getParentFile().mkdirs();
+		try {
+			file.createNewFile();
+			FileOutputStream mFileOutputStream = new FileOutputStream(file);
+			MessageDigest digest = MessageDigest.getInstance("SHA-1");
+			digest.reset();
+			DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+					mFileOutputStream, digest);
+			mDigestOutputStream.write(avatar.getImageAsBytes());
+			mDigestOutputStream.flush();
+			mDigestOutputStream.close();
+			avatar.size = file.length();
+			String sha1sum = CryptoHelper.bytesToHex(digest.digest());
+			if (sha1sum.equals(avatar.sha1sum)) {
+				file.renameTo(new File(filename));
+				return true;
+			} else {
+				Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
+				file.delete();
+				return false;
+			}
+		} catch (FileNotFoundException e) {
+			return false;
+		} catch (IOException e) {
+			return false;
+		} catch (NoSuchAlgorithmException e) {
+			return false;
+		}
+	}
+
+	public String getAvatarPath(String avatar) {
+		return mXmppConnectionService.getFilesDir().getAbsolutePath()
+				+ "/avatars/" + avatar;
+	}
+
+	public Uri getAvatarUri(String avatar) {
+		return Uri.parse("file:" + getAvatarPath(avatar));
+	}
+
+	public Bitmap cropCenterSquare(Uri image, int size) {
+		try {
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			options.inSampleSize = calcSampleSize(image, size);
+			InputStream is = mXmppConnectionService.getContentResolver()
+					.openInputStream(image);
+			Bitmap input = BitmapFactory.decodeStream(is, null, options);
+			if (input == null) {
+				return null;
+			} else {
+				int rotation = getRotation(image);
+				if (rotation > 0) {
+					input = rotate(input, rotation);
+				}
+				return cropCenterSquare(input, size);
+			}
+		} catch (FileNotFoundException e) {
+			return null;
+		}
+	}
+
+	public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
+		try {
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			options.inSampleSize = calcSampleSize(image,
+					Math.max(newHeight, newWidth));
+			InputStream is = mXmppConnectionService.getContentResolver()
+					.openInputStream(image);
+			Bitmap source = BitmapFactory.decodeStream(is, null, options);
+
+			int sourceWidth = source.getWidth();
+			int sourceHeight = source.getHeight();
+			float xScale = (float) newWidth / sourceWidth;
+			float yScale = (float) newHeight / sourceHeight;
+			float scale = Math.max(xScale, yScale);
+			float scaledWidth = scale * sourceWidth;
+			float scaledHeight = scale * sourceHeight;
+			float left = (newWidth - scaledWidth) / 2;
+			float top = (newHeight - scaledHeight) / 2;
+
+			RectF targetRect = new RectF(left, top, left + scaledWidth, top
+					+ scaledHeight);
+			Bitmap dest = Bitmap.createBitmap(newWidth, newHeight,
+					source.getConfig());
+			Canvas canvas = new Canvas(dest);
+			canvas.drawBitmap(source, null, targetRect, null);
+
+			return dest;
+		} catch (FileNotFoundException e) {
+			return null;
+		}
+
+	}
+
+	public Bitmap cropCenterSquare(Bitmap input, int size) {
+		int w = input.getWidth();
+		int h = input.getHeight();
+
+		float scale = Math.max((float) size / h, (float) size / w);
+
+		float outWidth = scale * w;
+		float outHeight = scale * h;
+		float left = (size - outWidth) / 2;
+		float top = (size - outHeight) / 2;
+		RectF target = new RectF(left, top, left + outWidth, top + outHeight);
+
+		Bitmap output = Bitmap.createBitmap(size, size, input.getConfig());
+		Canvas canvas = new Canvas(output);
+		canvas.drawBitmap(input, null, target, null);
+		return output;
+	}
+
+	private int calcSampleSize(Uri image, int size)
+			throws FileNotFoundException {
+		BitmapFactory.Options options = new BitmapFactory.Options();
+		options.inJustDecodeBounds = true;
+		BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver()
+				.openInputStream(image), null, options);
+		return calcSampleSize(options, size);
+	}
+
+	private int calcSampleSize(File image, int size) {
+		BitmapFactory.Options options = new BitmapFactory.Options();
+		options.inJustDecodeBounds = true;
+		BitmapFactory.decodeFile(image.getAbsolutePath(), options);
+		return calcSampleSize(options, size);
+	}
+
+	private int calcSampleSize(BitmapFactory.Options options, int size) {
+		int height = options.outHeight;
+		int width = options.outWidth;
+		int inSampleSize = 1;
+
+		if (height > size || width > size) {
+			int halfHeight = height / 2;
+			int halfWidth = width / 2;
+
+			while ((halfHeight / inSampleSize) > size
+					&& (halfWidth / inSampleSize) > size) {
+				inSampleSize *= 2;
+			}
+		}
+		return inSampleSize;
+	}
+
+	public Uri getJingleFileUri(Message message) {
+		File file = getFile(message);
+		return Uri.parse("file://" + file.getAbsolutePath());
+	}
+
+	public class ImageCopyException extends Exception {
+		private static final long serialVersionUID = -1010013599132881427L;
+		private int resId;
+
+		public ImageCopyException(int resId) {
+			this.resId = resId;
+		}
+
+		public int getResId() {
+			return resId;
+		}
+	}
+
+	public Bitmap getAvatar(String avatar, int size) {
+		if (avatar == null) {
+			return null;
+		}
+		Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
+		if (bm == null) {
+			return null;
+		}
+		return bm;
+	}
+
+	public boolean isFileAvailable(Message message) {
+		return getFile(message).exists();
+	}
+}

conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java 🔗

@@ -0,0 +1,23 @@
+package eu.siacs.conversations.services;
+
+public class AbstractConnectionManager {
+	protected XmppConnectionService mXmppConnectionService;
+
+	public AbstractConnectionManager(XmppConnectionService service) {
+		this.mXmppConnectionService = service;
+	}
+
+	public XmppConnectionService getXmppConnectionService() {
+		return this.mXmppConnectionService;
+	}
+
+	public long getAutoAcceptFileSize() {
+		String config = this.mXmppConnectionService.getPreferences().getString(
+				"auto_accept_file_size", "524288");
+		try {
+			return Long.parseLong(config);
+		} catch (NumberFormatException e) {
+			return 524288;
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java 🔗

@@ -0,0 +1,298 @@
+package eu.siacs.conversations.services;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.entities.MucOptions;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.util.Log;
+
+public class AvatarService {
+
+	private static final int FG_COLOR = 0xFFFAFAFA;
+	private static final int TRANSPARENT = 0x00000000;
+
+	private static final String PREFIX_CONTACT = "contact";
+	private static final String PREFIX_CONVERSATION = "conversation";
+	private static final String PREFIX_ACCOUNT = "account";
+	private static final String PREFIX_GENERIC = "generic";
+
+	private ArrayList<Integer> sizes = new ArrayList<Integer>();
+
+	protected XmppConnectionService mXmppConnectionService = null;
+
+	public AvatarService(XmppConnectionService service) {
+		this.mXmppConnectionService = service;
+	}
+
+	public Bitmap get(Contact contact, int size) {
+		final String KEY = key(contact, size);
+		Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
+		if (avatar != null) {
+			return avatar;
+		}
+		Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+		avatar = mXmppConnectionService.getFileBackend().getAvatar(
+				contact.getAvatar(), size);
+		if (avatar == null) {
+			if (contact.getProfilePhoto() != null) {
+				avatar = mXmppConnectionService.getFileBackend()
+						.cropCenterSquare(Uri.parse(contact.getProfilePhoto()),
+								size);
+				if (avatar == null) {
+					avatar = get(contact.getDisplayName(), size);
+				}
+			} else {
+				avatar = get(contact.getDisplayName(), size);
+			}
+		}
+		this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+		return avatar;
+	}
+
+	public void clear(Contact contact) {
+		for (Integer size : sizes) {
+			this.mXmppConnectionService.getBitmapCache().remove(
+					key(contact, size));
+		}
+	}
+
+	private String key(Contact contact, int size) {
+		synchronized (this.sizes) {
+			if (!this.sizes.contains(size)) {
+				this.sizes.add(size);
+			}
+		}
+		return PREFIX_CONTACT + "_" + contact.getAccount().getJid() + "_"
+				+ contact.getJid() + "_" + String.valueOf(size);
+	}
+
+	public Bitmap get(ListItem item, int size) {
+		if (item instanceof Contact) {
+			return get((Contact) item, size);
+		} else if (item instanceof Bookmark) {
+			Bookmark bookmark = (Bookmark) item;
+			if (bookmark.getConversation() != null) {
+				return get(bookmark.getConversation(), size);
+			} else {
+				return get(bookmark.getDisplayName(), size);
+			}
+		} else {
+			return get(item.getDisplayName(), size);
+		}
+	}
+
+	public Bitmap get(Conversation conversation, int size) {
+		if (conversation.getMode() == Conversation.MODE_SINGLE) {
+			return get(conversation.getContact(), size);
+		} else {
+			return get(conversation.getMucOptions(), size);
+		}
+	}
+
+	public void clear(Conversation conversation) {
+		if (conversation.getMode() == Conversation.MODE_SINGLE) {
+			clear(conversation.getContact());
+		} else {
+			clear(conversation.getMucOptions());
+		}
+	}
+
+	public Bitmap get(MucOptions mucOptions, int size) {
+		final String KEY = key(mucOptions, size);
+		Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
+		if (bitmap != null) {
+			return bitmap;
+		}
+		Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+		List<MucOptions.User> users = mucOptions.getUsers();
+		int count = users.size();
+		bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+		Canvas canvas = new Canvas(bitmap);
+		bitmap.eraseColor(TRANSPARENT);
+
+		if (count == 0) {
+			String name = mucOptions.getConversation().getName();
+			String letter = name.substring(0, 1);
+			int color = this.getColorForName(name);
+			drawTile(canvas, letter, color, 0, 0, size, size);
+		} else if (count == 1) {
+			drawTile(canvas, users.get(0), 0, 0, size, size);
+		} else if (count == 2) {
+			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+			drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
+		} else if (count == 3) {
+			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+			drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
+			drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size,
+					size);
+		} else if (count == 4) {
+			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+			drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+			drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+			drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size,
+					size);
+		} else {
+			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+			drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+			drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+			drawTile(canvas, "\u2026", 0xFF202020, size / 2 + 1, size / 2 + 1,
+					size, size);
+		}
+		this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+		return bitmap;
+	}
+
+	public void clear(MucOptions options) {
+		for (Integer size : sizes) {
+			this.mXmppConnectionService.getBitmapCache().remove(
+					key(options, size));
+		}
+	}
+
+	private String key(MucOptions options, int size) {
+		synchronized (this.sizes) {
+			if (!this.sizes.contains(size)) {
+				this.sizes.add(size);
+			}
+		}
+		return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid()
+				+ "_" + String.valueOf(size);
+	}
+
+	public Bitmap get(Account account, int size) {
+		final String KEY = key(account, size);
+		Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
+		if (avatar != null) {
+			return avatar;
+		}
+		Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+		avatar = mXmppConnectionService.getFileBackend().getAvatar(
+				account.getAvatar(), size);
+		if (avatar == null) {
+			avatar = get(account.getJid(), size);
+		}
+		mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+		return avatar;
+	}
+
+	public void clear(Account account) {
+		for (Integer size : sizes) {
+			this.mXmppConnectionService.getBitmapCache().remove(
+					key(account, size));
+		}
+	}
+
+	private String key(Account account, int size) {
+		synchronized (this.sizes) {
+			if (!this.sizes.contains(size)) {
+				this.sizes.add(size);
+			}
+		}
+		return PREFIX_ACCOUNT + "_" + account.getUuid() + "_"
+				+ String.valueOf(size);
+	}
+
+	public Bitmap get(String name, int size) {
+		final String KEY = key(name, size);
+		Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
+		if (bitmap != null) {
+			return bitmap;
+		}
+		Log.d(Config.LOGTAG, "no cache hit for " + KEY);
+		bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+		Canvas canvas = new Canvas(bitmap);
+		String letter = name.substring(0, 1);
+		int color = this.getColorForName(name);
+		drawTile(canvas, letter, color, 0, 0, size, size);
+		mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+		return bitmap;
+	}
+
+	private String key(String name, int size) {
+		synchronized (this.sizes) {
+			if (!this.sizes.contains(size)) {
+				this.sizes.add(size);
+			}
+		}
+		return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
+	}
+
+	private void drawTile(Canvas canvas, String letter, int tileColor,
+			int left, int top, int right, int bottom) {
+		letter = letter.toUpperCase(Locale.getDefault());
+		Paint tilePaint = new Paint(), textPaint = new Paint();
+		tilePaint.setColor(tileColor);
+		textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
+		textPaint.setColor(FG_COLOR);
+		textPaint.setTypeface(Typeface.create("sans-serif-light",
+				Typeface.NORMAL));
+		textPaint.setTextSize((float) ((right - left) * 0.8));
+		Rect rect = new Rect();
+
+		canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
+		textPaint.getTextBounds(letter, 0, 1, rect);
+		float width = textPaint.measureText(letter);
+		canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
+				/ 2 + rect.height() / 2, textPaint);
+	}
+
+	private void drawTile(Canvas canvas, MucOptions.User user, int left,
+			int top, int right, int bottom) {
+		Contact contact = user.getContact();
+		if (contact != null) {
+			Uri uri = null;
+			if (contact.getAvatar() != null) {
+				uri = mXmppConnectionService.getFileBackend().getAvatarUri(
+						contact.getAvatar());
+			} else if (contact.getProfilePhoto() != null) {
+				uri = Uri.parse(contact.getProfilePhoto());
+			}
+			if (uri != null) {
+				Bitmap bitmap = mXmppConnectionService.getFileBackend()
+						.cropCenter(uri, bottom - top, right - left);
+				if (bitmap != null) {
+					drawTile(canvas, bitmap, left, top, right, bottom);
+				} else {
+					String letter = user.getName().substring(0, 1);
+					int color = this.getColorForName(user.getName());
+					drawTile(canvas, letter, color, left, top, right, bottom);
+				}
+			} else {
+				String letter = user.getName().substring(0, 1);
+				int color = this.getColorForName(user.getName());
+				drawTile(canvas, letter, color, left, top, right, bottom);
+			}
+		} else {
+			String letter = user.getName().substring(0, 1);
+			int color = this.getColorForName(user.getName());
+			drawTile(canvas, letter, color, left, top, right, bottom);
+		}
+	}
+
+	private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop,
+			int dstright, int dstbottom) {
+		Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
+		canvas.drawBitmap(bm, null, dst, null);
+	}
+
+	private int getColorForName(String name) {
+		int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
+				0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
+				0xFF795548, 0xFF607d8b };
+		return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)];
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java 🔗

@@ -0,0 +1,24 @@
+package eu.siacs.conversations.services;
+
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class EventReceiver extends BroadcastReceiver {
+	@Override
+	public void onReceive(Context context, Intent intent) {
+		Intent mIntentForService = new Intent(context,
+				XmppConnectionService.class);
+		if (intent.getAction() != null) {
+			mIntentForService.setAction(intent.getAction());
+		} else {
+			mIntentForService.setAction("other");
+		}
+		if (intent.getAction().equals("ui")
+				|| DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
+			context.startService(mIntentForService);
+		}
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -0,0 +1,237 @@
+package eu.siacs.conversations.services;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.Html;
+import android.util.DisplayMetrics;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+
+public class NotificationService {
+
+	private XmppConnectionService mXmppConnectionService;
+
+	private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>();
+
+	public int NOTIFICATION_ID = 0x2342;
+	private Conversation mOpenConversation;
+	private boolean mIsInForeground;
+
+	public NotificationService(XmppConnectionService service) {
+		this.mXmppConnectionService = service;
+	}
+
+	public void push(Message message) {
+		PowerManager pm = (PowerManager) mXmppConnectionService
+				.getSystemService(Context.POWER_SERVICE);
+		boolean isScreenOn = pm.isScreenOn();
+
+		if (this.mIsInForeground && isScreenOn
+				&& this.mOpenConversation == message.getConversation()) {
+			return;
+		}
+		synchronized (notifications) {
+			String conversationUuid = message.getConversationUuid();
+			if (notifications.containsKey(conversationUuid)) {
+				notifications.get(conversationUuid).add(message);
+			} else {
+				ArrayList<Message> mList = new ArrayList<Message>();
+				mList.add(message);
+				notifications.put(conversationUuid, mList);
+			}
+			Account account = message.getConversation().getAccount();
+			updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
+					&& !account.inGracePeriod());
+		}
+
+	}
+
+	public void clear() {
+		synchronized (notifications) {
+			notifications.clear();
+			updateNotification(false);
+		}
+	}
+
+	public void clear(Conversation conversation) {
+		synchronized (notifications) {
+			notifications.remove(conversation.getUuid());
+			updateNotification(false);
+		}
+	}
+
+	private void updateNotification(boolean notify) {
+		NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
+				.getSystemService(Context.NOTIFICATION_SERVICE);
+		SharedPreferences preferences = mXmppConnectionService.getPreferences();
+
+		String ringtone = preferences.getString("notification_ringtone", null);
+		boolean vibrate = preferences.getBoolean("vibrate_on_notification",
+				true);
+
+		if (notifications.size() == 0) {
+			notificationManager.cancel(NOTIFICATION_ID);
+		} else {
+			NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
+					mXmppConnectionService);
+			mBuilder.setSmallIcon(R.drawable.ic_notification);
+			if (notifications.size() == 1) {
+				ArrayList<Message> messages = notifications.values().iterator()
+						.next();
+				if (messages.size() >= 1) {
+					Conversation conversation = messages.get(0)
+							.getConversation();
+					mBuilder.setLargeIcon(mXmppConnectionService
+							.getAvatarService().get(conversation, getPixel(64)));
+					mBuilder.setContentTitle(conversation.getName());
+					StringBuilder text = new StringBuilder();
+					for (int i = 0; i < messages.size(); ++i) {
+						text.append(messages.get(i).getReadableBody(
+								mXmppConnectionService));
+						if (i != messages.size() - 1) {
+							text.append("\n");
+						}
+					}
+					mBuilder.setStyle(new NotificationCompat.BigTextStyle()
+							.bigText(text.toString()));
+					mBuilder.setContentText(messages.get(0).getReadableBody(
+							mXmppConnectionService));
+					if (notify) {
+						mBuilder.setTicker(messages.get(messages.size() - 1)
+								.getReadableBody(mXmppConnectionService));
+					}
+					mBuilder.setContentIntent(createContentIntent(conversation
+							.getUuid()));
+				} else {
+					notificationManager.cancel(NOTIFICATION_ID);
+					return;
+				}
+			} else {
+				NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+				style.setBigContentTitle(notifications.size()
+						+ " "
+						+ mXmppConnectionService
+								.getString(R.string.unread_conversations));
+				StringBuilder names = new StringBuilder();
+				Conversation conversation = null;
+				for (ArrayList<Message> messages : notifications.values()) {
+					if (messages.size() > 0) {
+						conversation = messages.get(0).getConversation();
+						String name = conversation.getName();
+						style.addLine(Html.fromHtml("<b>"
+								+ name
+								+ "</b> "
+								+ messages.get(0).getReadableBody(
+										mXmppConnectionService)));
+						names.append(name);
+						names.append(", ");
+					}
+				}
+				if (names.length() >= 2) {
+					names.delete(names.length() - 2, names.length());
+				}
+				mBuilder.setContentTitle(notifications.size()
+						+ " "
+						+ mXmppConnectionService
+								.getString(R.string.unread_conversations));
+				mBuilder.setContentText(names.toString());
+				mBuilder.setStyle(style);
+				if (conversation != null) {
+					mBuilder.setContentIntent(createContentIntent(conversation
+							.getUuid()));
+				}
+			}
+			if (notify) {
+				if (vibrate) {
+					int dat = 70;
+					long[] pattern = { 0, 3 * dat, dat, dat };
+					mBuilder.setVibrate(pattern);
+				}
+				if (ringtone != null) {
+					mBuilder.setSound(Uri.parse(ringtone));
+				}
+			}
+			mBuilder.setDeleteIntent(createDeleteIntent());
+			mBuilder.setLights(0xffffffff, 2000, 4000);
+			Notification notification = mBuilder.build();
+			notificationManager.notify(NOTIFICATION_ID, notification);
+		}
+	}
+
+	private PendingIntent createContentIntent(String conversationUuid) {
+		TaskStackBuilder stackBuilder = TaskStackBuilder
+				.create(mXmppConnectionService);
+		stackBuilder.addParentStack(ConversationActivity.class);
+
+		Intent viewConversationIntent = new Intent(mXmppConnectionService,
+				ConversationActivity.class);
+		viewConversationIntent.setAction(Intent.ACTION_VIEW);
+		viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+				conversationUuid);
+		viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+
+		stackBuilder.addNextIntent(viewConversationIntent);
+
+		PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
+				PendingIntent.FLAG_UPDATE_CURRENT);
+		return resultPendingIntent;
+	}
+
+	private PendingIntent createDeleteIntent() {
+		Intent intent = new Intent(mXmppConnectionService,
+				XmppConnectionService.class);
+		intent.setAction("clear_notification");
+		return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
+	}
+
+	public static boolean wasHighlightedOrPrivate(Message message) {
+		String nick = message.getConversation().getMucOptions().getActualNick();
+		Pattern highlight = generateNickHighlightPattern(nick);
+		if (message.getBody() == null || nick == null) {
+			return false;
+		}
+		Matcher m = highlight.matcher(message.getBody());
+		return (m.find() || message.getType() == Message.TYPE_PRIVATE);
+	}
+
+	private static Pattern generateNickHighlightPattern(String nick) {
+		// We expect a word boundary, i.e. space or start of string, followed by
+		// the
+		// nick (matched in case-insensitive manner), followed by optional
+		// punctuation (for example "bob: i disagree" or "how are you alice?"),
+		// followed by another word boundary.
+		return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b",
+				Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+	}
+
+	public void setOpenConversation(Conversation conversation) {
+		this.mOpenConversation = conversation;
+	}
+
+	public void setIsInForeground(boolean foreground) {
+		this.mIsInForeground = foreground;
+	}
+
+	private int getPixel(int dp) {
+		DisplayMetrics metrics = mXmppConnectionService.getResources()
+				.getDisplayMetrics();
+		return ((int) (dp * metrics.density));
+	}
+}

conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -0,0 +1,1927 @@
+package eu.siacs.conversations.services;
+
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.generator.IqGenerator;
+import eu.siacs.conversations.generator.MessageGenerator;
+import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.parser.MessageParser;
+import eu.siacs.conversations.parser.PresenceParser;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
+import eu.siacs.conversations.utils.PRNGFixes;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnBindListener;
+import eu.siacs.conversations.xmpp.OnContactStatusChanged;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
+import eu.siacs.conversations.xmpp.OnStatusChanged;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
+import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.FileObserver;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+import android.util.LruCache;
+
+public class XmppConnectionService extends Service {
+
+	public DatabaseBackend databaseBackend;
+	private FileBackend fileBackend = new FileBackend(this);
+
+	public long startDate;
+
+	private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
+	public static String ACTION_CLEAR_NOTIFICATION = "clear_notification";
+
+	private MemorizingTrustManager mMemorizingTrustManager;
+
+	private NotificationService mNotificationService = new NotificationService(
+			this);
+
+	private MessageParser mMessageParser = new MessageParser(this);
+	private PresenceParser mPresenceParser = new PresenceParser(this);
+	private IqParser mIqParser = new IqParser(this);
+	private MessageGenerator mMessageGenerator = new MessageGenerator(this);
+	private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
+
+	private List<Account> accounts;
+	private CopyOnWriteArrayList<Conversation> conversations = null;
+	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
+			this);
+	private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
+			this);
+	private AvatarService mAvatarService = new AvatarService(this);
+
+	private OnConversationUpdate mOnConversationUpdate = null;
+	private Integer convChangedListenerCount = 0;
+	private OnAccountUpdate mOnAccountUpdate = null;
+	private Integer accountChangedListenerCount = 0;
+	private OnRosterUpdate mOnRosterUpdate = null;
+	private Integer rosterChangedListenerCount = 0;
+	public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
+
+		@Override
+		public void onContactStatusChanged(Contact contact, boolean online) {
+			Conversation conversation = find(getConversations(), contact);
+			if (conversation != null) {
+				conversation.endOtrIfNeeded();
+				if (online && (contact.getPresences().size() == 1)) {
+					sendUnsendMessages(conversation);
+				}
+			}
+		}
+	};
+
+	private SecureRandom mRandom;
+
+	private ContentObserver contactObserver = new ContentObserver(null) {
+		@Override
+		public void onChange(boolean selfChange) {
+			super.onChange(selfChange);
+			Intent intent = new Intent(getApplicationContext(),
+					XmppConnectionService.class);
+			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
+			startService(intent);
+		}
+	};
+
+	private FileObserver fileObserver = new FileObserver(
+			FileBackend.getConversationsDirectory()) {
+
+		@Override
+		public void onEvent(int event, String path) {
+			if (event == FileObserver.DELETE) {
+				markFileDeleted(path.split("\\.")[0]);
+			}
+		}
+	};
+
+	private final IBinder mBinder = new XmppConnectionBinder();
+	private OnStatusChanged statusListener = new OnStatusChanged() {
+
+		@Override
+		public void onStatusChanged(Account account) {
+			XmppConnection connection = account.getXmppConnection();
+			if (mOnAccountUpdate != null) {
+				mOnAccountUpdate.onAccountUpdate();
+				;
+			}
+			if (account.getStatus() == Account.STATUS_ONLINE) {
+				for (Conversation conversation : account.pendingConferenceLeaves) {
+					leaveMuc(conversation);
+				}
+				for (Conversation conversation : account.pendingConferenceJoins) {
+					joinMuc(conversation);
+				}
+				mJingleConnectionManager.cancelInTransmission();
+				List<Conversation> conversations = getConversations();
+				for (int i = 0; i < conversations.size(); ++i) {
+					if (conversations.get(i).getAccount() == account) {
+						conversations.get(i).startOtrIfNeeded();
+						sendUnsendMessages(conversations.get(i));
+					}
+				}
+				if (connection != null && connection.getFeatures().csi()) {
+					if (checkListeners()) {
+						Log.d(Config.LOGTAG, account.getJid()
+								+ " sending csi//inactive");
+						connection.sendInactive();
+					} else {
+						Log.d(Config.LOGTAG, account.getJid()
+								+ " sending csi//active");
+						connection.sendActive();
+					}
+				}
+				syncDirtyContacts(account);
+				scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
+			} else if (account.getStatus() == Account.STATUS_OFFLINE) {
+				resetSendingToWaiting(account);
+				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+					int timeToReconnect = mRandom.nextInt(50) + 10;
+					scheduleWakeupCall(timeToReconnect, false);
+				}
+			} else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
+				databaseBackend.updateAccount(account);
+				reconnectAccount(account, true);
+			} else if ((account.getStatus() != Account.STATUS_CONNECTING)
+					&& (account.getStatus() != Account.STATUS_NO_INTERNET)) {
+				if (connection != null) {
+					int next = connection.getTimeToNextAttempt();
+					Log.d(Config.LOGTAG, account.getJid()
+							+ ": error connecting account. try again in "
+							+ next + "s for the "
+							+ (connection.getAttempt() + 1) + " time");
+					scheduleWakeupCall((int) (next * 1.2), false);
+				}
+			}
+			UIHelper.showErrorNotification(getApplicationContext(),
+					getAccounts());
+		}
+	};
+
+	private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
+
+		@Override
+		public void onJinglePacketReceived(Account account, JinglePacket packet) {
+			mJingleConnectionManager.deliverPacket(account, packet);
+		}
+	};
+
+	private OpenPgpServiceConnection pgpServiceConnection;
+	private PgpEngine mPgpEngine = null;
+	private Intent pingIntent;
+	private PendingIntent pendingPingIntent = null;
+	private WakeLock wakeLock;
+	private PowerManager pm;
+	private OnBindListener mOnBindListener = new OnBindListener() {
+
+		@Override
+		public void onBind(final Account account) {
+			account.getRoster().clearPresences();
+			account.clearPresences(); // self presences
+			account.pendingConferenceJoins.clear();
+			account.pendingConferenceLeaves.clear();
+			fetchRosterFromServer(account);
+			fetchBookmarks(account);
+			sendPresencePacket(account,
+					mPresenceGenerator.sendPresence(account));
+			connectMultiModeConversations(account);
+			updateConversationUi();
+		}
+	};
+
+	private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
+
+		@Override
+		public void onMessageAcknowledged(Account account, String uuid) {
+			for (Conversation conversation : getConversations()) {
+				if (conversation.getAccount() == account) {
+					for (Message message : conversation.getMessages()) {
+						if ((message.getStatus() == Message.STATUS_UNSEND || message
+								.getStatus() == Message.STATUS_WAITING)
+								&& message.getUuid().equals(uuid)) {
+							markMessage(message, Message.STATUS_SEND);
+							return;
+						}
+					}
+				}
+			}
+		}
+	};
+	private LruCache<String, Bitmap> mBitmapCache;
+
+	public PgpEngine getPgpEngine() {
+		if (pgpServiceConnection.isBound()) {
+			if (this.mPgpEngine == null) {
+				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
+						getApplicationContext(),
+						pgpServiceConnection.getService()), this);
+			}
+			return mPgpEngine;
+		} else {
+			return null;
+		}
+
+	}
+
+	public FileBackend getFileBackend() {
+		return this.fileBackend;
+	}
+
+	public AvatarService getAvatarService() {
+		return this.mAvatarService;
+	}
+
+	public Message attachImageToConversation(final Conversation conversation,
+			final Uri uri, final UiCallback<Message> callback) {
+		final Message message;
+		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+			message = new Message(conversation, "",
+					Message.ENCRYPTION_DECRYPTED);
+		} else {
+			message = new Message(conversation, "",
+					conversation.getNextEncryption(forceEncryption()));
+		}
+		message.setPresence(conversation.getNextPresence());
+		message.setType(Message.TYPE_IMAGE);
+		message.setStatus(Message.STATUS_OFFERED);
+		new Thread(new Runnable() {
+
+			@Override
+			public void run() {
+				try {
+					getFileBackend().copyImageToPrivateStorage(message, uri);
+					if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+						getPgpEngine().encrypt(message, callback);
+					} else {
+						callback.success(message);
+					}
+				} catch (FileBackend.ImageCopyException e) {
+					callback.error(e.getResId(), message);
+				}
+			}
+		}).start();
+		return message;
+	}
+
+	public Conversation find(Bookmark bookmark) {
+		return find(bookmark.getAccount(), bookmark.getJid());
+	}
+
+	public Conversation find(Account account, String jid) {
+		return find(getConversations(), account, jid);
+	}
+
+	public class XmppConnectionBinder extends Binder {
+		public XmppConnectionService getService() {
+			return XmppConnectionService.this;
+		}
+	}
+
+	@Override
+	public int onStartCommand(Intent intent, int flags, int startId) {
+		if (intent != null && intent.getAction() != null) {
+			if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) {
+				mergePhoneContactsWithRoster();
+				return START_STICKY;
+			} else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) {
+				logoutAndSave();
+				return START_NOT_STICKY;
+			} else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) {
+				mNotificationService.clear();
+			}
+		}
+		this.wakeLock.acquire();
+
+		for (Account account : accounts) {
+			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+				if (!hasInternetConnection()) {
+					account.setStatus(Account.STATUS_NO_INTERNET);
+					if (statusListener != null) {
+						statusListener.onStatusChanged(account);
+					}
+				} else {
+					if (account.getStatus() == Account.STATUS_NO_INTERNET) {
+						account.setStatus(Account.STATUS_OFFLINE);
+						if (statusListener != null) {
+							statusListener.onStatusChanged(account);
+						}
+					}
+					if (account.getStatus() == Account.STATUS_ONLINE) {
+						long lastReceived = account.getXmppConnection()
+								.getLastPacketReceived();
+						long lastSent = account.getXmppConnection()
+								.getLastPingSent();
+						if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) {
+							Log.d(Config.LOGTAG, account.getJid()
+									+ ": ping timeout");
+							this.reconnectAccount(account, true);
+						} else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) {
+							account.getXmppConnection().sendPing();
+							this.scheduleWakeupCall(2, false);
+						}
+					} else if (account.getStatus() == Account.STATUS_OFFLINE) {
+						if (account.getXmppConnection() == null) {
+							account.setXmppConnection(this
+									.createConnection(account));
+						}
+						new Thread(account.getXmppConnection()).start();
+					} else if ((account.getStatus() == Account.STATUS_CONNECTING)
+							&& ((SystemClock.elapsedRealtime() - account
+									.getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) {
+						Log.d(Config.LOGTAG, account.getJid()
+								+ ": time out during connect reconnecting");
+						reconnectAccount(account, true);
+					} else {
+						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
+							reconnectAccount(account, true);
+						}
+					}
+					// in any case. reschedule wakup call
+					this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
+				}
+				if (mOnAccountUpdate != null) {
+					mOnAccountUpdate.onAccountUpdate();
+				}
+			}
+		}
+		if (wakeLock.isHeld()) {
+			try {
+				wakeLock.release();
+			} catch (RuntimeException re) {
+			}
+		}
+		return START_STICKY;
+	}
+
+	public boolean hasInternetConnection() {
+		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
+				.getSystemService(Context.CONNECTIVITY_SERVICE);
+		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+		return activeNetwork != null && activeNetwork.isConnected();
+	}
+
+	@SuppressLint("TrulyRandom")
+	@Override
+	public void onCreate() {
+		ExceptionHelper.init(getApplicationContext());
+		PRNGFixes.apply();
+		this.mRandom = new SecureRandom();
+		this.mMemorizingTrustManager = new MemorizingTrustManager(
+				getApplicationContext());
+
+		int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+		int cacheSize = maxMemory / 8;
+		this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
+			@Override
+			protected int sizeOf(String key, Bitmap bitmap) {
+				return bitmap.getByteCount() / 1024;
+			}
+		};
+
+		this.databaseBackend = DatabaseBackend
+				.getInstance(getApplicationContext());
+		this.accounts = databaseBackend.getAccounts();
+
+		for (Account account : this.accounts) {
+			this.databaseBackend.readRoster(account.getRoster());
+		}
+		this.mergePhoneContactsWithRoster();
+		this.getConversations();
+
+		getContentResolver().registerContentObserver(
+				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+		this.fileObserver.startWatching();
+		this.pgpServiceConnection = new OpenPgpServiceConnection(
+				getApplicationContext(), "org.sufficientlysecure.keychain");
+		this.pgpServiceConnection.bindToService();
+
+		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+				"XmppConnectionService");
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		this.logoutAndSave();
+	}
+
+	@Override
+	public void onTaskRemoved(Intent rootIntent) {
+		super.onTaskRemoved(rootIntent);
+		this.logoutAndSave();
+	}
+
+	private void logoutAndSave() {
+		for (Account account : accounts) {
+			databaseBackend.writeRoster(account.getRoster());
+			if (account.getXmppConnection() != null) {
+				disconnect(account, false);
+			}
+		}
+		Context context = getApplicationContext();
+		AlarmManager alarmManager = (AlarmManager) context
+				.getSystemService(Context.ALARM_SERVICE);
+		Intent intent = new Intent(context, EventReceiver.class);
+		alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
+		Log.d(Config.LOGTAG, "good bye");
+		stopSelf();
+	}
+
+	protected void scheduleWakeupCall(int seconds, boolean ping) {
+		long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
+		Context context = getApplicationContext();
+		AlarmManager alarmManager = (AlarmManager) context
+				.getSystemService(Context.ALARM_SERVICE);
+
+		if (ping) {
+			if (this.pingIntent == null) {
+				this.pingIntent = new Intent(context, EventReceiver.class);
+				this.pingIntent.setAction("ping");
+				this.pingIntent.putExtra("time", timeToWake);
+				this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
+						this.pingIntent, 0);
+				alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+						timeToWake, pendingPingIntent);
+			} else {
+				long scheduledTime = this.pingIntent.getLongExtra("time", 0);
+				if (scheduledTime < SystemClock.elapsedRealtime()
+						|| (scheduledTime > timeToWake)) {
+					this.pingIntent.putExtra("time", timeToWake);
+					alarmManager.cancel(this.pendingPingIntent);
+					this.pendingPingIntent = PendingIntent.getBroadcast(
+							context, 0, this.pingIntent, 0);
+					alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+							timeToWake, pendingPingIntent);
+				}
+			}
+		} else {
+			Intent intent = new Intent(context, EventReceiver.class);
+			intent.setAction("ping_check");
+			PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
+					intent, 0);
+			alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
+					alarmIntent);
+		}
+
+	}
+
+	public XmppConnection createConnection(Account account) {
+		SharedPreferences sharedPref = getPreferences();
+		account.setResource(sharedPref.getString("resource", "mobile")
+				.toLowerCase(Locale.getDefault()));
+		XmppConnection connection = new XmppConnection(account, this);
+		connection.setOnMessagePacketReceivedListener(this.mMessageParser);
+		connection.setOnStatusChangedListener(this.statusListener);
+		connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
+		connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
+		connection.setOnJinglePacketReceivedListener(this.jingleListener);
+		connection.setOnBindListener(this.mOnBindListener);
+		connection
+				.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
+		return connection;
+	}
+
+	public void sendMessage(Message message) {
+		Account account = message.getConversation().getAccount();
+		account.deactivateGracePeriod();
+		Conversation conv = message.getConversation();
+		MessagePacket packet = null;
+		boolean saveInDb = true;
+		boolean send = false;
+		if (account.getStatus() == Account.STATUS_ONLINE
+				&& account.getXmppConnection() != null) {
+			if (message.getType() == Message.TYPE_IMAGE) {
+				if (message.getPresence() != null) {
+					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+						if (!conv.hasValidOtrSession()
+								&& (message.getPresence() != null)) {
+							conv.startOtrSession(this, message.getPresence(),
+									true);
+							message.setStatus(Message.STATUS_WAITING);
+						} else if (conv.hasValidOtrSession()
+								&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+							mJingleConnectionManager
+									.createNewConnection(message);
+						} else if (message.getPresence() == null) {
+							message.setStatus(Message.STATUS_WAITING);
+						}
+					} else {
+						mJingleConnectionManager.createNewConnection(message);
+					}
+				} else {
+					message.setStatus(Message.STATUS_WAITING);
+				}
+			} else {
+				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+					if (!conv.hasValidOtrSession()
+							&& (message.getPresence() != null)) {
+						conv.startOtrSession(this, message.getPresence(), true);
+						message.setStatus(Message.STATUS_WAITING);
+					} else if (conv.hasValidOtrSession()
+							&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+						message.setPresence(conv.getOtrSession().getSessionID()
+								.getUserID());
+						packet = mMessageGenerator.generateOtrChat(message);
+						send = true;
+
+					} else if (message.getPresence() == null) {
+						message.setStatus(Message.STATUS_WAITING);
+					}
+				} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+					message.getConversation().endOtrIfNeeded();
+					failWaitingOtrMessages(message.getConversation());
+					packet = mMessageGenerator.generatePgpChat(message);
+					send = true;
+				} else {
+					message.getConversation().endOtrIfNeeded();
+					failWaitingOtrMessages(message.getConversation());
+					packet = mMessageGenerator.generateChat(message);
+					send = true;
+				}
+			}
+			if (!account.getXmppConnection().getFeatures().sm()
+					&& conv.getMode() != Conversation.MODE_MULTI) {
+				message.setStatus(Message.STATUS_SEND);
+			}
+		} else {
+			message.setStatus(Message.STATUS_WAITING);
+			if (message.getType() == Message.TYPE_TEXT) {
+				if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+					String pgpBody = message.getEncryptedBody();
+					String decryptedBody = message.getBody();
+					message.setBody(pgpBody);
+					message.setEncryption(Message.ENCRYPTION_PGP);
+					databaseBackend.createMessage(message);
+					saveInDb = false;
+					message.setBody(decryptedBody);
+					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+					if (conv.hasValidOtrSession()) {
+						message.setPresence(conv.getOtrSession().getSessionID()
+								.getUserID());
+					} else if (!conv.hasValidOtrSession()
+							&& message.getPresence() != null) {
+						conv.startOtrSession(this, message.getPresence(), false);
+					}
+				}
+			}
+
+		}
+		conv.add(message);
+		if (saveInDb) {
+			if (message.getEncryption() == Message.ENCRYPTION_NONE
+					|| saveEncryptedMessages()) {
+				databaseBackend.createMessage(message);
+			}
+		}
+		if ((send) && (packet != null)) {
+			sendMessagePacket(account, packet);
+		}
+		updateConversationUi();
+	}
+
+	private void sendUnsendMessages(Conversation conversation) {
+		for (int i = 0; i < conversation.getMessages().size(); ++i) {
+			int status = conversation.getMessages().get(i).getStatus();
+			if (status == Message.STATUS_WAITING) {
+				resendMessage(conversation.getMessages().get(i));
+			}
+		}
+	}
+
+	private void resendMessage(Message message) {
+		Account account = message.getConversation().getAccount();
+		MessagePacket packet = null;
+		if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+			Presences presences = message.getConversation().getContact()
+					.getPresences();
+			if (!message.getConversation().hasValidOtrSession()) {
+				if ((message.getPresence() != null)
+						&& (presences.has(message.getPresence()))) {
+					message.getConversation().startOtrSession(this,
+							message.getPresence(), true);
+				} else {
+					if (presences.size() == 1) {
+						String presence = presences.asStringArray()[0];
+						message.getConversation().startOtrSession(this,
+								presence, true);
+					}
+				}
+			} else {
+				if (message.getConversation().getOtrSession()
+						.getSessionStatus() == SessionStatus.ENCRYPTED) {
+					if (message.getType() == Message.TYPE_TEXT) {
+						packet = mMessageGenerator.generateOtrChat(message,
+								true);
+					} else if (message.getType() == Message.TYPE_IMAGE) {
+						mJingleConnectionManager.createNewConnection(message);
+					}
+				}
+			}
+		} else if (message.getType() == Message.TYPE_TEXT) {
+			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+				packet = mMessageGenerator.generateChat(message, true);
+			} else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
+					|| (message.getEncryption() == Message.ENCRYPTION_PGP)) {
+				packet = mMessageGenerator.generatePgpChat(message, true);
+			}
+		} else if (message.getType() == Message.TYPE_IMAGE) {
+			Presences presences = message.getConversation().getContact()
+					.getPresences();
+			if ((message.getPresence() != null)
+					&& (presences.has(message.getPresence()))) {
+				markMessage(message, Message.STATUS_OFFERED);
+				mJingleConnectionManager.createNewConnection(message);
+			} else {
+				if (presences.size() == 1) {
+					String presence = presences.asStringArray()[0];
+					message.setPresence(presence);
+					markMessage(message, Message.STATUS_OFFERED);
+					mJingleConnectionManager.createNewConnection(message);
+				}
+			}
+		}
+		if (packet != null) {
+			if (!account.getXmppConnection().getFeatures().sm()
+					&& message.getConversation().getMode() != Conversation.MODE_MULTI) {
+				markMessage(message, Message.STATUS_SEND);
+			} else {
+				markMessage(message, Message.STATUS_UNSEND);
+			}
+			sendMessagePacket(account, packet);
+		}
+	}
+
+	public void fetchRosterFromServer(Account account) {
+		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+		if (!"".equals(account.getRosterVersion())) {
+			Log.d(Config.LOGTAG, account.getJid()
+					+ ": fetching roster version " + account.getRosterVersion());
+		} else {
+			Log.d(Config.LOGTAG, account.getJid() + ": fetching roster");
+		}
+		iqPacket.query("jabber:iq:roster").setAttribute("ver",
+				account.getRosterVersion());
+		account.getXmppConnection().sendIqPacket(iqPacket,
+				new OnIqPacketReceived() {
+
+					@Override
+					public void onIqPacketReceived(final Account account,
+							IqPacket packet) {
+						Element query = packet.findChild("query");
+						if (query != null) {
+							account.getRoster().markAllAsNotInRoster();
+							mIqParser.rosterItems(account, query);
+						}
+					}
+				});
+	}
+
+	public void fetchBookmarks(Account account) {
+		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+		Element query = iqPacket.query("jabber:iq:private");
+		query.addChild("storage", "storage:bookmarks");
+		OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				Element query = packet.query();
+				List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
+				Element storage = query.findChild("storage",
+						"storage:bookmarks");
+				if (storage != null) {
+					for (Element item : storage.getChildren()) {
+						if (item.getName().equals("conference")) {
+							Bookmark bookmark = Bookmark.parse(item, account);
+							bookmarks.add(bookmark);
+							Conversation conversation = find(bookmark);
+							if (conversation != null) {
+								conversation.setBookmark(bookmark);
+							} else {
+								if (bookmark.autojoin()) {
+									conversation = findOrCreateConversation(
+											account, bookmark.getJid(), true);
+									conversation.setBookmark(bookmark);
+									joinMuc(conversation);
+								}
+							}
+						}
+					}
+				}
+				account.setBookmarks(bookmarks);
+			}
+		};
+		sendIqPacket(account, iqPacket, callback);
+
+	}
+
+	public void pushBookmarks(Account account) {
+		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET);
+		Element query = iqPacket.query("jabber:iq:private");
+		Element storage = query.addChild("storage", "storage:bookmarks");
+		for (Bookmark bookmark : account.getBookmarks()) {
+			storage.addChild(bookmark);
+		}
+		sendIqPacket(account, iqPacket, null);
+	}
+
+	private void mergePhoneContactsWithRoster() {
+		PhoneHelper.loadPhoneContacts(getApplicationContext(),
+				new OnPhoneContactsLoadedListener() {
+					@Override
+					public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
+						for (Account account : accounts) {
+							account.getRoster().clearSystemAccounts();
+						}
+						for (Bundle phoneContact : phoneContacts) {
+							for (Account account : accounts) {
+								String jid = phoneContact.getString("jid");
+								Contact contact = account.getRoster()
+										.getContact(jid);
+								String systemAccount = phoneContact
+										.getInt("phoneid")
+										+ "#"
+										+ phoneContact.getString("lookup");
+								contact.setSystemAccount(systemAccount);
+								contact.setPhotoUri(phoneContact
+										.getString("photouri"));
+								contact.setSystemName(phoneContact
+										.getString("displayname"));
+								getAvatarService().clear(contact);
+							}
+						}
+					}
+				});
+	}
+
+	public List<Conversation> getConversations() {
+		if (this.conversations == null) {
+			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
+			for (Account account : this.accounts) {
+				accountLookupTable.put(account.getUuid(), account);
+			}
+			this.conversations = databaseBackend
+					.getConversations(Conversation.STATUS_AVAILABLE);
+			for (Conversation conv : this.conversations) {
+				Account account = accountLookupTable.get(conv.getAccountUuid());
+				conv.setAccount(account);
+				conv.setMessages(databaseBackend.getMessages(conv, 50));
+				checkDeletedFiles(conv);
+			}
+		}
+		return this.conversations;
+	}
+
+	private void checkDeletedFiles(Conversation conversation) {
+		for (Message message : conversation.getMessages()) {
+			if (message.getType() == Message.TYPE_IMAGE
+					&& message.getEncryption() != Message.ENCRYPTION_PGP) {
+				if (!getFileBackend().isFileAvailable(message)) {
+					message.setDownloadable(new DeletedDownloadable());
+				}
+			}
+		}
+	}
+
+	private void markFileDeleted(String uuid) {
+		for (Conversation conversation : getConversations()) {
+			for (Message message : conversation.getMessages()) {
+				if (message.getType() == Message.TYPE_IMAGE
+						&& message.getEncryption() != Message.ENCRYPTION_PGP
+						&& message.getUuid().equals(uuid)) {
+					if (!getFileBackend().isFileAvailable(message)) {
+						message.setDownloadable(new DeletedDownloadable());
+						updateConversationUi();
+					}
+					return;
+				}
+			}
+		}
+	}
+
+	public void populateWithOrderedConversations(List<Conversation> list) {
+		populateWithOrderedConversations(list, true);
+	}
+
+	public void populateWithOrderedConversations(List<Conversation> list,
+			boolean includeConferences) {
+		list.clear();
+		if (includeConferences) {
+			list.addAll(getConversations());
+		} else {
+			for (Conversation conversation : getConversations()) {
+				if (conversation.getMode() == Conversation.MODE_SINGLE) {
+					list.add(conversation);
+				}
+			}
+		}
+		Collections.sort(list, new Comparator<Conversation>() {
+			@Override
+			public int compare(Conversation lhs, Conversation rhs) {
+				Message left = lhs.getLatestMessage();
+				Message right = rhs.getLatestMessage();
+				if (left.getTimeSent() > right.getTimeSent()) {
+					return -1;
+				} else if (left.getTimeSent() < right.getTimeSent()) {
+					return 1;
+				} else {
+					return 0;
+				}
+			}
+		});
+	}
+
+	public int loadMoreMessages(Conversation conversation, long timestamp) {
+		List<Message> messages = databaseBackend.getMessages(conversation, 50,
+				timestamp);
+		for (Message message : messages) {
+			message.setConversation(conversation);
+		}
+		conversation.addAll(0, messages);
+		return messages.size();
+	}
+
+	public List<Account> getAccounts() {
+		return this.accounts;
+	}
+
+	public Conversation find(List<Conversation> haystack, Contact contact) {
+		for (Conversation conversation : haystack) {
+			if (conversation.getContact() == contact) {
+				return conversation;
+			}
+		}
+		return null;
+	}
+
+	public Conversation find(List<Conversation> haystack, Account account,
+			String jid) {
+		for (Conversation conversation : haystack) {
+			if ((account == null || conversation.getAccount().equals(account))
+					&& (conversation.getContactJid().split("/", 2)[0]
+							.equals(jid))) {
+				return conversation;
+			}
+		}
+		return null;
+	}
+
+	public Conversation findOrCreateConversation(Account account, String jid,
+			boolean muc) {
+		Conversation conversation = find(account, jid);
+		if (conversation != null) {
+			return conversation;
+		}
+		conversation = databaseBackend.findConversation(account, jid);
+		if (conversation != null) {
+			conversation.setStatus(Conversation.STATUS_AVAILABLE);
+			conversation.setAccount(account);
+			if (muc) {
+				conversation.setMode(Conversation.MODE_MULTI);
+			} else {
+				conversation.setMode(Conversation.MODE_SINGLE);
+			}
+			conversation.setMessages(databaseBackend.getMessages(conversation,
+					50));
+			this.databaseBackend.updateConversation(conversation);
+		} else {
+			String conversationName;
+			Contact contact = account.getRoster().getContact(jid);
+			if (contact != null) {
+				conversationName = contact.getDisplayName();
+			} else {
+				conversationName = jid.split("@")[0];
+			}
+			if (muc) {
+				conversation = new Conversation(conversationName, account, jid,
+						Conversation.MODE_MULTI);
+			} else {
+				conversation = new Conversation(conversationName, account, jid,
+						Conversation.MODE_SINGLE);
+			}
+			this.databaseBackend.createConversation(conversation);
+		}
+		this.conversations.add(conversation);
+		updateConversationUi();
+		return conversation;
+	}
+
+	public void archiveConversation(Conversation conversation) {
+		if (conversation.getMode() == Conversation.MODE_MULTI) {
+			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+				Bookmark bookmark = conversation.getBookmark();
+				if (bookmark != null && bookmark.autojoin()) {
+					bookmark.setAutojoin(false);
+					pushBookmarks(bookmark.getAccount());
+				}
+			}
+			leaveMuc(conversation);
+		} else {
+			conversation.endOtrIfNeeded();
+		}
+		this.databaseBackend.updateConversation(conversation);
+		this.conversations.remove(conversation);
+		updateConversationUi();
+	}
+
+	public void clearConversationHistory(Conversation conversation) {
+		this.databaseBackend.deleteMessagesInConversation(conversation);
+		this.fileBackend.removeFiles(conversation);
+		conversation.getMessages().clear();
+		updateConversationUi();
+	}
+
+	public int getConversationCount() {
+		return this.databaseBackend.getConversationCount();
+	}
+
+	public void createAccount(Account account) {
+		databaseBackend.createAccount(account);
+		this.accounts.add(account);
+		this.reconnectAccount(account, false);
+		updateAccountUi();
+	}
+
+	public void updateAccount(Account account) {
+		this.statusListener.onStatusChanged(account);
+		databaseBackend.updateAccount(account);
+		reconnectAccount(account, false);
+		updateAccountUi();
+		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
+	}
+
+	public void deleteAccount(Account account) {
+		for (Conversation conversation : conversations) {
+			if (conversation.getAccount() == account) {
+				if (conversation.getMode() == Conversation.MODE_MULTI) {
+					leaveMuc(conversation);
+				} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
+					conversation.endOtrIfNeeded();
+				}
+				conversations.remove(conversation);
+			}
+		}
+		if (account.getXmppConnection() != null) {
+			this.disconnect(account, true);
+		}
+		databaseBackend.deleteAccount(account);
+		this.accounts.remove(account);
+		updateAccountUi();
+		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
+	}
+
+	public void setOnConversationListChangedListener(
+			OnConversationUpdate listener) {
+		if (!isScreenOn()) {
+			Log.d(Config.LOGTAG,
+					"ignoring setOnConversationListChangedListener");
+			return;
+		}
+		synchronized (this.convChangedListenerCount) {
+			if (checkListeners()) {
+				switchToForeground();
+			}
+			this.mOnConversationUpdate = listener;
+			this.mNotificationService.setIsInForeground(true);
+			this.convChangedListenerCount++;
+		}
+	}
+
+	public void removeOnConversationListChangedListener() {
+		synchronized (this.convChangedListenerCount) {
+			this.convChangedListenerCount--;
+			if (this.convChangedListenerCount <= 0) {
+				this.convChangedListenerCount = 0;
+				this.mOnConversationUpdate = null;
+				this.mNotificationService.setIsInForeground(false);
+				if (checkListeners()) {
+					switchToBackground();
+				}
+			}
+		}
+	}
+
+	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
+		if (!isScreenOn()) {
+			Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
+			return;
+		}
+		synchronized (this.accountChangedListenerCount) {
+			if (checkListeners()) {
+				switchToForeground();
+			}
+			this.mOnAccountUpdate = listener;
+			this.accountChangedListenerCount++;
+		}
+	}
+
+	public void removeOnAccountListChangedListener() {
+		synchronized (this.accountChangedListenerCount) {
+			this.accountChangedListenerCount--;
+			if (this.accountChangedListenerCount <= 0) {
+				this.mOnAccountUpdate = null;
+				this.accountChangedListenerCount = 0;
+				if (checkListeners()) {
+					switchToBackground();
+				}
+			}
+		}
+	}
+
+	public void setOnRosterUpdateListener(OnRosterUpdate listener) {
+		if (!isScreenOn()) {
+			Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
+			return;
+		}
+		synchronized (this.rosterChangedListenerCount) {
+			if (checkListeners()) {
+				switchToForeground();
+			}
+			this.mOnRosterUpdate = listener;
+			this.rosterChangedListenerCount++;
+		}
+	}
+
+	public void removeOnRosterUpdateListener() {
+		synchronized (this.rosterChangedListenerCount) {
+			this.rosterChangedListenerCount--;
+			if (this.rosterChangedListenerCount <= 0) {
+				this.rosterChangedListenerCount = 0;
+				this.mOnRosterUpdate = null;
+				if (checkListeners()) {
+					switchToBackground();
+				}
+			}
+		}
+	}
+
+	private boolean checkListeners() {
+		return (this.mOnAccountUpdate == null
+				&& this.mOnConversationUpdate == null && this.mOnRosterUpdate == null);
+	}
+
+	private void switchToForeground() {
+		for (Account account : getAccounts()) {
+			if (account.getStatus() == Account.STATUS_ONLINE) {
+				XmppConnection connection = account.getXmppConnection();
+				if (connection != null && connection.getFeatures().csi()) {
+					connection.sendActive();
+				}
+			}
+		}
+		Log.d(Config.LOGTAG, "app switched into foreground");
+	}
+
+	private void switchToBackground() {
+		for (Account account : getAccounts()) {
+			if (account.getStatus() == Account.STATUS_ONLINE) {
+				XmppConnection connection = account.getXmppConnection();
+				if (connection != null && connection.getFeatures().csi()) {
+					connection.sendInactive();
+				}
+			}
+		}
+		this.mNotificationService.setIsInForeground(false);
+		Log.d(Config.LOGTAG, "app switched into background");
+	}
+
+	private boolean isScreenOn() {
+		PowerManager pm = (PowerManager) this
+				.getSystemService(Context.POWER_SERVICE);
+		return pm.isScreenOn();
+	}
+
+	public void connectMultiModeConversations(Account account) {
+		List<Conversation> conversations = getConversations();
+		for (int i = 0; i < conversations.size(); i++) {
+			Conversation conversation = conversations.get(i);
+			if ((conversation.getMode() == Conversation.MODE_MULTI)
+					&& (conversation.getAccount() == account)) {
+				joinMuc(conversation);
+			}
+		}
+	}
+
+	public void joinMuc(Conversation conversation) {
+		Account account = conversation.getAccount();
+		account.pendingConferenceJoins.remove(conversation);
+		account.pendingConferenceLeaves.remove(conversation);
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			Log.d(Config.LOGTAG,
+					"joining conversation " + conversation.getContactJid());
+			String nick = conversation.getMucOptions().getProposedNick();
+			conversation.getMucOptions().setJoinNick(nick);
+			PresencePacket packet = new PresencePacket();
+			String joinJid = conversation.getMucOptions().getJoinJid();
+			packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
+			Element x = new Element("x");
+			x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
+			if (conversation.getMucOptions().getPassword() != null) {
+				Element password = x.addChild("password");
+				password.setContent(conversation.getMucOptions().getPassword());
+			}
+			String sig = account.getPgpSignature();
+			if (sig != null) {
+				packet.addChild("status").setContent("online");
+				packet.addChild("x", "jabber:x:signed").setContent(sig);
+			}
+			if (conversation.getMessages().size() != 0) {
+				final SimpleDateFormat mDateFormat = new SimpleDateFormat(
+						"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+				mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+				Date date = new Date(conversation.getLatestMessage()
+						.getTimeSent() + 1000);
+				x.addChild("history").setAttribute("since",
+						mDateFormat.format(date));
+			}
+			packet.addChild(x);
+			sendPresencePacket(account, packet);
+			if (!joinJid.equals(conversation.getContactJid())) {
+				conversation.setContactJid(joinJid);
+				databaseBackend.updateConversation(conversation);
+			}
+		} else {
+			account.pendingConferenceJoins.add(conversation);
+		}
+	}
+
+	private OnRenameListener renameListener = null;
+	private IqGenerator mIqGenerator = new IqGenerator(this);
+
+	public void setOnRenameListener(OnRenameListener listener) {
+		this.renameListener = listener;
+	}
+
+	public void providePasswordForMuc(Conversation conversation, String password) {
+		if (conversation.getMode() == Conversation.MODE_MULTI) {
+			conversation.getMucOptions().setPassword(password);
+			if (conversation.getBookmark() != null) {
+				conversation.getBookmark().setAutojoin(true);
+				pushBookmarks(conversation.getAccount());
+			}
+			databaseBackend.updateConversation(conversation);
+			joinMuc(conversation);
+		}
+	}
+
+	public void renameInMuc(final Conversation conversation, final String nick) {
+		final MucOptions options = conversation.getMucOptions();
+		options.setJoinNick(nick);
+		if (options.online()) {
+			Account account = conversation.getAccount();
+			options.setOnRenameListener(new OnRenameListener() {
+
+				@Override
+				public void onRename(boolean success) {
+					if (renameListener != null) {
+						renameListener.onRename(success);
+					}
+					if (success) {
+						conversation.setContactJid(conversation.getMucOptions()
+								.getJoinJid());
+						databaseBackend.updateConversation(conversation);
+						Bookmark bookmark = conversation.getBookmark();
+						if (bookmark != null) {
+							bookmark.setNick(nick);
+							pushBookmarks(bookmark.getAccount());
+						}
+					}
+				}
+			});
+			options.flagAboutToRename();
+			PresencePacket packet = new PresencePacket();
+			packet.setAttribute("to", options.getJoinJid());
+			packet.setAttribute("from", conversation.getAccount().getFullJid());
+
+			String sig = account.getPgpSignature();
+			if (sig != null) {
+				packet.addChild("status").setContent("online");
+				packet.addChild("x", "jabber:x:signed").setContent(sig);
+			}
+			sendPresencePacket(account, packet);
+		} else {
+			conversation.setContactJid(options.getJoinJid());
+			databaseBackend.updateConversation(conversation);
+			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+				Bookmark bookmark = conversation.getBookmark();
+				if (bookmark != null) {
+					bookmark.setNick(nick);
+					pushBookmarks(bookmark.getAccount());
+				}
+				joinMuc(conversation);
+			}
+		}
+	}
+
+	public void leaveMuc(Conversation conversation) {
+		Account account = conversation.getAccount();
+		account.pendingConferenceJoins.remove(conversation);
+		account.pendingConferenceLeaves.remove(conversation);
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			PresencePacket packet = new PresencePacket();
+			packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
+			packet.setAttribute("from", conversation.getAccount().getFullJid());
+			packet.setAttribute("type", "unavailable");
+			sendPresencePacket(conversation.getAccount(), packet);
+			conversation.getMucOptions().setOffline();
+			conversation.deregisterWithBookmark();
+			Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+					+ ": leaving muc " + conversation.getContactJid());
+		} else {
+			account.pendingConferenceLeaves.add(conversation);
+		}
+	}
+
+	public void disconnect(Account account, boolean force) {
+		if ((account.getStatus() == Account.STATUS_ONLINE)
+				|| (account.getStatus() == Account.STATUS_DISABLED)) {
+			if (!force) {
+				List<Conversation> conversations = getConversations();
+				for (int i = 0; i < conversations.size(); i++) {
+					Conversation conversation = conversations.get(i);
+					if (conversation.getAccount() == account) {
+						if (conversation.getMode() == Conversation.MODE_MULTI) {
+							leaveMuc(conversation);
+						} else {
+							if (conversation.endOtrIfNeeded()) {
+								Log.d(Config.LOGTAG, account.getJid()
+										+ ": ended otr session with "
+										+ conversation.getContactJid());
+							}
+						}
+					}
+				}
+			}
+			account.getXmppConnection().disconnect(force);
+		}
+	}
+
+	@Override
+	public IBinder onBind(Intent intent) {
+		return mBinder;
+	}
+
+	public void updateMessage(Message message) {
+		databaseBackend.updateMessage(message);
+		updateConversationUi();
+	}
+
+	protected void syncDirtyContacts(Account account) {
+		for (Contact contact : account.getRoster().getContacts()) {
+			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
+				pushContactToServer(contact);
+			}
+			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
+				deleteContactOnServer(contact);
+			}
+		}
+	}
+
+	public void createContact(Contact contact) {
+		SharedPreferences sharedPref = getPreferences();
+		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
+		if (autoGrant) {
+			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+			contact.setOption(Contact.Options.ASKING);
+		}
+		pushContactToServer(contact);
+	}
+
+	public void onOtrSessionEstablished(Conversation conversation) {
+		Account account = conversation.getAccount();
+		List<Message> messages = conversation.getMessages();
+		Session otrSession = conversation.getOtrSession();
+		Log.d(Config.LOGTAG,
+				account.getJid() + " otr session established with "
+						+ conversation.getContactJid() + "/"
+						+ otrSession.getSessionID().getUserID());
+		for (int i = 0; i < messages.size(); ++i) {
+			Message msg = messages.get(i);
+			if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
+					&& (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
+				msg.setPresence(otrSession.getSessionID().getUserID());
+				if (msg.getType() == Message.TYPE_TEXT) {
+					MessagePacket outPacket = mMessageGenerator
+							.generateOtrChat(msg, true);
+					if (outPacket != null) {
+						msg.setStatus(Message.STATUS_SEND);
+						databaseBackend.updateMessage(msg);
+						sendMessagePacket(account, outPacket);
+					}
+				} else if (msg.getType() == Message.TYPE_IMAGE) {
+					mJingleConnectionManager.createNewConnection(msg);
+				}
+			}
+		}
+		updateConversationUi();
+	}
+
+	public boolean renewSymmetricKey(Conversation conversation) {
+		Account account = conversation.getAccount();
+		byte[] symmetricKey = new byte[32];
+		this.mRandom.nextBytes(symmetricKey);
+		Session otrSession = conversation.getOtrSession();
+		if (otrSession != null) {
+			MessagePacket packet = new MessagePacket();
+			packet.setType(MessagePacket.TYPE_CHAT);
+			packet.setFrom(account.getFullJid());
+			packet.addChild("private", "urn:xmpp:carbons:2");
+			packet.addChild("no-copy", "urn:xmpp:hints");
+			packet.setTo(otrSession.getSessionID().getAccountID() + "/"
+					+ otrSession.getSessionID().getUserID());
+			try {
+				packet.setBody(otrSession
+						.transformSending(CryptoHelper.FILETRANSFER
+								+ CryptoHelper.bytesToHex(symmetricKey)));
+				sendMessagePacket(account, packet);
+				conversation.setSymmetricKey(symmetricKey);
+				return true;
+			} catch (OtrException e) {
+				return false;
+			}
+		}
+		return false;
+	}
+
+	public void pushContactToServer(Contact contact) {
+		contact.resetOption(Contact.Options.DIRTY_DELETE);
+		contact.setOption(Contact.Options.DIRTY_PUSH);
+		Account account = contact.getAccount();
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			boolean ask = contact.getOption(Contact.Options.ASKING);
+			boolean sendUpdates = contact
+					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
+					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
+			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+			iq.query("jabber:iq:roster").addChild(contact.asElement());
+			account.getXmppConnection().sendIqPacket(iq, null);
+			if (sendUpdates) {
+				sendPresencePacket(account,
+						mPresenceGenerator.sendPresenceUpdatesTo(contact));
+			}
+			if (ask) {
+				sendPresencePacket(account,
+						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
+			}
+		}
+	}
+
+	public void publishAvatar(Account account, Uri image,
+			final UiCallback<Avatar> callback) {
+		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
+		final int size = Config.AVATAR_SIZE;
+		final Avatar avatar = getFileBackend()
+				.getPepAvatar(image, size, format);
+		if (avatar != null) {
+			avatar.height = size;
+			avatar.width = size;
+			if (format.equals(Bitmap.CompressFormat.WEBP)) {
+				avatar.type = "image/webp";
+			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
+				avatar.type = "image/jpeg";
+			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
+				avatar.type = "image/png";
+			}
+			if (!getFileBackend().save(avatar)) {
+				callback.error(R.string.error_saving_avatar, avatar);
+				return;
+			}
+			IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
+			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+				@Override
+				public void onIqPacketReceived(Account account, IqPacket result) {
+					if (result.getType() == IqPacket.TYPE_RESULT) {
+						IqPacket packet = XmppConnectionService.this.mIqGenerator
+								.publishAvatarMetadata(avatar);
+						sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+							@Override
+							public void onIqPacketReceived(Account account,
+									IqPacket result) {
+								if (result.getType() == IqPacket.TYPE_RESULT) {
+									if (account.setAvatar(avatar.getFilename())) {
+										databaseBackend.updateAccount(account);
+									}
+									callback.success(avatar);
+								} else {
+									callback.error(
+											R.string.error_publish_avatar_server_reject,
+											avatar);
+								}
+							}
+						});
+					} else {
+						callback.error(
+								R.string.error_publish_avatar_server_reject,
+								avatar);
+					}
+				}
+			});
+		} else {
+			callback.error(R.string.error_publish_avatar_converting, null);
+		}
+	}
+
+	public void fetchAvatar(Account account, Avatar avatar) {
+		fetchAvatar(account, avatar, null);
+	}
+
+	public void fetchAvatar(Account account, final Avatar avatar,
+			final UiCallback<Avatar> callback) {
+		IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
+		sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket result) {
+				final String ERROR = account.getJid()
+						+ ": fetching avatar for " + avatar.owner + " failed ";
+				if (result.getType() == IqPacket.TYPE_RESULT) {
+					avatar.image = mIqParser.avatarData(result);
+					if (avatar.image != null) {
+						if (getFileBackend().save(avatar)) {
+							if (account.getJid().equals(avatar.owner)) {
+								if (account.setAvatar(avatar.getFilename())) {
+									databaseBackend.updateAccount(account);
+								}
+								getAvatarService().clear(account);
+								updateConversationUi();
+								updateAccountUi();
+							} else {
+								Contact contact = account.getRoster()
+										.getContact(avatar.owner);
+								contact.setAvatar(avatar.getFilename());
+								getAvatarService().clear(contact);
+								updateConversationUi();
+								updateRosterUi();
+							}
+							if (callback != null) {
+								callback.success(avatar);
+							}
+							Log.d(Config.LOGTAG, account.getJid()
+									+ ": succesfully fetched avatar for "
+									+ avatar.owner);
+							return;
+						}
+					} else {
+
+						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
+					}
+				} else {
+					Element error = result.findChild("error");
+					if (error == null) {
+						Log.d(Config.LOGTAG, ERROR + "(server error)");
+					} else {
+						Log.d(Config.LOGTAG, ERROR + error.toString());
+					}
+				}
+				if (callback != null) {
+					callback.error(0, null);
+				}
+
+			}
+		});
+	}
+
+	public void checkForAvatar(Account account,
+			final UiCallback<Avatar> callback) {
+		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
+		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				if (packet.getType() == IqPacket.TYPE_RESULT) {
+					Element pubsub = packet.findChild("pubsub",
+							"http://jabber.org/protocol/pubsub");
+					if (pubsub != null) {
+						Element items = pubsub.findChild("items");
+						if (items != null) {
+							Avatar avatar = Avatar.parseMetadata(items);
+							if (avatar != null) {
+								avatar.owner = account.getJid();
+								if (fileBackend.isAvatarCached(avatar)) {
+									if (account.setAvatar(avatar.getFilename())) {
+										databaseBackend.updateAccount(account);
+									}
+									getAvatarService().clear(account);
+									callback.success(avatar);
+								} else {
+									fetchAvatar(account, avatar, callback);
+								}
+								return;
+							}
+						}
+					}
+				}
+				callback.error(0, null);
+			}
+		});
+	}
+
+	public void deleteContactOnServer(Contact contact) {
+		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+		contact.resetOption(Contact.Options.DIRTY_PUSH);
+		contact.setOption(Contact.Options.DIRTY_DELETE);
+		Account account = contact.getAccount();
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+			Element item = iq.query("jabber:iq:roster").addChild("item");
+			item.setAttribute("jid", contact.getJid());
+			item.setAttribute("subscription", "remove");
+			account.getXmppConnection().sendIqPacket(iq, null);
+		}
+	}
+
+	public void updateConversation(Conversation conversation) {
+		this.databaseBackend.updateConversation(conversation);
+	}
+
+	public void reconnectAccount(final Account account, final boolean force) {
+		new Thread(new Runnable() {
+
+			@Override
+			public void run() {
+				if (account.getXmppConnection() != null) {
+					disconnect(account, force);
+				}
+				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+					if (account.getXmppConnection() == null) {
+						account.setXmppConnection(createConnection(account));
+					}
+					Thread thread = new Thread(account.getXmppConnection());
+					thread.start();
+					scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
+							false);
+				} else {
+					account.getRoster().clearPresences();
+					account.setXmppConnection(null);
+				}
+			}
+		}).start();
+	}
+
+	public void invite(Conversation conversation, String contact) {
+		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
+		sendMessagePacket(conversation.getAccount(), packet);
+	}
+
+	public void resetSendingToWaiting(Account account) {
+		for (Conversation conversation : getConversations()) {
+			if (conversation.getAccount() == account) {
+				for (Message message : conversation.getMessages()) {
+					if (message.getType() != Message.TYPE_IMAGE
+							&& message.getStatus() == Message.STATUS_UNSEND) {
+						markMessage(message, Message.STATUS_WAITING);
+					}
+				}
+			}
+		}
+	}
+
+	public boolean markMessage(Account account, String recipient, String uuid,
+			int status) {
+		if (uuid == null) {
+			return false;
+		} else {
+			for (Conversation conversation : getConversations()) {
+				if (conversation.getContactJid().equals(recipient)
+						&& conversation.getAccount().equals(account)) {
+					return markMessage(conversation, uuid, status);
+				}
+			}
+			return false;
+		}
+	}
+
+	public boolean markMessage(Conversation conversation, String uuid,
+			int status) {
+		if (uuid == null) {
+			return false;
+		} else {
+			for (Message message : conversation.getMessages()) {
+				if (uuid.equals(message.getUuid())
+						|| (message.getStatus() >= Message.STATUS_SEND && uuid
+								.equals(message.getRemoteMsgId()))) {
+					markMessage(message, status);
+					return true;
+				}
+			}
+			return false;
+		}
+	}
+
+	public void markMessage(Message message, int status) {
+		if (status == Message.STATUS_SEND_FAILED
+				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
+						.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
+			return;
+		}
+		message.setStatus(status);
+		databaseBackend.updateMessage(message);
+		updateConversationUi();
+	}
+
+	public SharedPreferences getPreferences() {
+		return PreferenceManager
+				.getDefaultSharedPreferences(getApplicationContext());
+	}
+
+	public boolean forceEncryption() {
+		return getPreferences().getBoolean("force_encryption", false);
+	}
+
+	public boolean confirmMessages() {
+		return getPreferences().getBoolean("confirm_messages", true);
+	}
+
+	public boolean saveEncryptedMessages() {
+		return !getPreferences().getBoolean("dont_save_encrypted", false);
+	}
+
+	public boolean indicateReceived() {
+		return getPreferences().getBoolean("indicate_received", false);
+	}
+
+	public void updateConversationUi() {
+		if (mOnConversationUpdate != null) {
+			mOnConversationUpdate.onConversationUpdate();
+		}
+	}
+
+	public void updateAccountUi() {
+		if (mOnAccountUpdate != null) {
+			mOnAccountUpdate.onAccountUpdate();
+		}
+	}
+
+	public void updateRosterUi() {
+		if (mOnRosterUpdate != null) {
+			mOnRosterUpdate.onRosterUpdate();
+		}
+	}
+
+	public Account findAccountByJid(String accountJid) {
+		for (Account account : this.accounts) {
+			if (account.getJid().equals(accountJid)) {
+				return account;
+			}
+		}
+		return null;
+	}
+
+	public Conversation findConversationByUuid(String uuid) {
+		for (Conversation conversation : getConversations()) {
+			if (conversation.getUuid().equals(uuid)) {
+				return conversation;
+			}
+		}
+		return null;
+	}
+
+	public void markRead(Conversation conversation, boolean calledByUi) {
+		mNotificationService.clear(conversation);
+		String id = conversation.getLatestMarkableMessageId();
+		conversation.markRead();
+		if (confirmMessages() && id != null && calledByUi) {
+			Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+					+ ": sending read marker for " + conversation.getName());
+			Account account = conversation.getAccount();
+			String to = conversation.getContactJid();
+			this.sendMessagePacket(conversation.getAccount(),
+					mMessageGenerator.confirm(account, to, id));
+		}
+		if (!calledByUi) {
+			updateConversationUi();
+		}
+	}
+
+	public void failWaitingOtrMessages(Conversation conversation) {
+		for (Message message : conversation.getMessages()) {
+			if (message.getEncryption() == Message.ENCRYPTION_OTR
+					&& message.getStatus() == Message.STATUS_WAITING) {
+				markMessage(message, Message.STATUS_SEND_FAILED);
+			}
+		}
+	}
+
+	public SecureRandom getRNG() {
+		return this.mRandom;
+	}
+
+	public MemorizingTrustManager getMemorizingTrustManager() {
+		return this.mMemorizingTrustManager;
+	}
+
+	public PowerManager getPowerManager() {
+		return this.pm;
+	}
+
+	public LruCache<String, Bitmap> getBitmapCache() {
+		return this.mBitmapCache;
+	}
+
+	public void replyWithNotAcceptable(Account account, MessagePacket packet) {
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			MessagePacket error = this.mMessageGenerator
+					.generateNotAcceptable(packet);
+			sendMessagePacket(account, error);
+		}
+	}
+
+	public void syncRosterToDisk(final Account account) {
+		new Thread(new Runnable() {
+
+			@Override
+			public void run() {
+				databaseBackend.writeRoster(account.getRoster());
+			}
+		}).start();
+
+	}
+
+	public List<String> getKnownHosts() {
+		List<String> hosts = new ArrayList<String>();
+		for (Account account : getAccounts()) {
+			if (!hosts.contains(account.getServer())) {
+				hosts.add(account.getServer());
+			}
+			for (Contact contact : account.getRoster().getContacts()) {
+				if (contact.showInRoster()) {
+					String server = contact.getServer();
+					if (server != null && !hosts.contains(server)) {
+						hosts.add(server);
+					}
+				}
+			}
+		}
+		return hosts;
+	}
+
+	public List<String> getKnownConferenceHosts() {
+		ArrayList<String> mucServers = new ArrayList<String>();
+		for (Account account : accounts) {
+			if (account.getXmppConnection() != null) {
+				String server = account.getXmppConnection().getMucServer();
+				if (server != null && !mucServers.contains(server)) {
+					mucServers.add(server);
+				}
+			}
+		}
+		return mucServers;
+	}
+
+	public void sendMessagePacket(Account account, MessagePacket packet) {
+		XmppConnection connection = account.getXmppConnection();
+		if (connection != null) {
+			connection.sendMessagePacket(packet);
+		}
+	}
+
+	public void sendPresencePacket(Account account, PresencePacket packet) {
+		XmppConnection connection = account.getXmppConnection();
+		if (connection != null) {
+			connection.sendPresencePacket(packet);
+		}
+	}
+
+	public void sendIqPacket(Account account, IqPacket packet,
+			OnIqPacketReceived callback) {
+		XmppConnection connection = account.getXmppConnection();
+		if (connection != null) {
+			connection.sendIqPacket(packet, callback);
+		}
+	}
+
+	public MessageGenerator getMessageGenerator() {
+		return this.mMessageGenerator;
+	}
+
+	public PresenceGenerator getPresenceGenerator() {
+		return this.mPresenceGenerator;
+	}
+
+	public IqGenerator getIqGenerator() {
+		return this.mIqGenerator;
+	}
+
+	public JingleConnectionManager getJingleConnectionManager() {
+		return this.mJingleConnectionManager;
+	}
+
+	public interface OnConversationUpdate {
+		public void onConversationUpdate();
+	}
+
+	public interface OnAccountUpdate {
+		public void onAccountUpdate();
+	}
+
+	public interface OnRosterUpdate {
+		public void onRosterUpdate();
+	}
+
+	public List<Contact> findContacts(String jid) {
+		ArrayList<Contact> contacts = new ArrayList<Contact>();
+		for (Account account : getAccounts()) {
+			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+				Contact contact = account.getRoster().getContactFromRoster(jid);
+				if (contact != null) {
+					contacts.add(contact);
+				}
+			}
+		}
+		return contacts;
+	}
+
+	public NotificationService getNotificationService() {
+		return this.mNotificationService;
+	}
+
+	public HttpConnectionManager getHttpConnectionManager() {
+		return this.mHttpConnectionManager;
+	}
+
+	private class DeletedDownloadable implements Downloadable {
+
+		@Override
+		public boolean start() {
+			return false;
+		}
+
+		@Override
+		public int getStatus() {
+			return Downloadable.STATUS_DELETED;
+		}
+
+		@Override
+		public long getFileSize() {
+			return 0;
+		}
+
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java 🔗

@@ -0,0 +1,145 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.adapter.ListItemAdapter;
+
+public class ChooseContactActivity extends XmppActivity {
+
+	private ListView mListView;
+	private ArrayList<ListItem> contacts = new ArrayList<ListItem>();
+	private ArrayAdapter<ListItem> mContactsAdapter;
+
+	private EditText mSearchEditText;
+
+	private TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+		@Override
+		public void afterTextChanged(Editable editable) {
+			filterContacts(editable.toString());
+		}
+
+		@Override
+		public void beforeTextChanged(CharSequence s, int start, int count,
+				int after) {
+		}
+
+		@Override
+		public void onTextChanged(CharSequence s, int start, int before,
+				int count) {
+		}
+	};
+
+	private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
+
+		@Override
+		public boolean onMenuItemActionExpand(MenuItem item) {
+			mSearchEditText.post(new Runnable() {
+
+				@Override
+				public void run() {
+					mSearchEditText.requestFocus();
+					InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+					imm.showSoftInput(mSearchEditText,
+							InputMethodManager.SHOW_IMPLICIT);
+				}
+			});
+
+			return true;
+		}
+
+		@Override
+		public boolean onMenuItemActionCollapse(MenuItem item) {
+			InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+			imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+					InputMethodManager.HIDE_IMPLICIT_ONLY);
+			mSearchEditText.setText("");
+			filterContacts(null);
+			return true;
+		}
+	};
+
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.activity_choose_contact);
+		mListView = (ListView) findViewById(R.id.choose_contact_list);
+		mListView.setFastScrollEnabled(true);
+		mContactsAdapter = new ListItemAdapter(this, contacts);
+		mListView.setAdapter(mContactsAdapter);
+		mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+			@Override
+			public void onItemClick(AdapterView<?> arg0, View arg1,
+					int position, long arg3) {
+				InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+				imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+						InputMethodManager.HIDE_IMPLICIT_ONLY);
+				Intent request = getIntent();
+				Intent data = new Intent();
+				ListItem mListItem = contacts.get(position);
+				data.putExtra("contact", mListItem.getJid());
+				String account = request.getStringExtra("account");
+				if (account == null && mListItem instanceof Contact) {
+					account = ((Contact) mListItem).getAccount().getJid();
+				}
+				data.putExtra("account", account);
+				data.putExtra("conversation",
+						request.getStringExtra("conversation"));
+				setResult(RESULT_OK, data);
+				finish();
+			}
+		});
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.choose_contact, menu);
+		MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search);
+		View mSearchView = menuSearchView.getActionView();
+		mSearchEditText = (EditText) mSearchView
+				.findViewById(R.id.search_field);
+		mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+		menuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+		return true;
+	}
+
+	@Override
+	void onBackendConnected() {
+		filterContacts(null);
+	}
+
+	protected void filterContacts(String needle) {
+		this.contacts.clear();
+		for (Account account : xmppConnectionService.getAccounts()) {
+			if (account.getStatus() != Account.STATUS_DISABLED) {
+				for (Contact contact : account.getRoster().getContacts()) {
+					if (contact.showInRoster() && contact.match(needle)) {
+						this.contacts.add(contact);
+					}
+				}
+			}
+		}
+		Collections.sort(this.contacts);
+		mContactsAdapter.notifyDataSetChanged();
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java 🔗

@@ -0,0 +1,280 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.MucOptions.User;
+import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.IntentSender.SendIntentException;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class ConferenceDetailsActivity extends XmppActivity {
+	public static final String ACTION_VIEW_MUC = "view_muc";
+	private Conversation conversation;
+	private TextView mYourNick;
+	private ImageView mYourPhoto;
+	private ImageButton mEditNickButton;
+	private TextView mRoleAffiliaton;
+	private TextView mFullJid;
+	private TextView mAccountJid;
+	private LinearLayout membersView;
+	private LinearLayout mMoreDetails;
+	private Button mInviteButton;
+	private String uuid = null;
+
+	private OnClickListener inviteListener = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			inviteToConversation(conversation);
+		}
+	};
+
+	private List<User> users = new ArrayList<MucOptions.User>();
+	private OnConversationUpdate onConvChanged = new OnConversationUpdate() {
+
+		@Override
+		public void onConversationUpdate() {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					populateView();
+				}
+			});
+		}
+	};
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.activity_muc_details);
+		mYourNick = (TextView) findViewById(R.id.muc_your_nick);
+		mYourPhoto = (ImageView) findViewById(R.id.your_photo);
+		mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button);
+		mFullJid = (TextView) findViewById(R.id.muc_jabberid);
+		membersView = (LinearLayout) findViewById(R.id.muc_members);
+		mAccountJid = (TextView) findViewById(R.id.details_account);
+		mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details);
+		mMoreDetails.setVisibility(View.GONE);
+		mInviteButton = (Button) findViewById(R.id.invite);
+		mInviteButton.setOnClickListener(inviteListener);
+		getActionBar().setHomeButtonEnabled(true);
+		getActionBar().setDisplayHomeAsUpEnabled(true);
+		mEditNickButton.setOnClickListener(new OnClickListener() {
+
+			@Override
+			public void onClick(View v) {
+				quickEdit(conversation.getMucOptions().getActualNick(),
+						new OnValueEdited() {
+
+							@Override
+							public void onValueEdited(String value) {
+								xmppConnectionService.renameInMuc(conversation,
+										value);
+							}
+						});
+			}
+		});
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(MenuItem menuItem) {
+		switch (menuItem.getItemId()) {
+		case android.R.id.home:
+			finish();
+			break;
+		case R.id.action_edit_subject:
+			if (conversation != null) {
+				quickEdit(conversation.getName(), new OnValueEdited() {
+
+					@Override
+					public void onValueEdited(String value) {
+						MessagePacket packet = xmppConnectionService
+								.getMessageGenerator().conferenceSubject(
+										conversation, value);
+						xmppConnectionService.sendMessagePacket(
+								conversation.getAccount(), packet);
+					}
+				});
+			}
+			break;
+		}
+		return super.onOptionsItemSelected(menuItem);
+	}
+
+	public String getReadableRole(int role) {
+		switch (role) {
+		case User.ROLE_MODERATOR:
+			return getString(R.string.moderator);
+		case User.ROLE_PARTICIPANT:
+			return getString(R.string.participant);
+		case User.ROLE_VISITOR:
+			return getString(R.string.visitor);
+		default:
+			return "";
+		}
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.muc_details, menu);
+		return true;
+	}
+
+	@Override
+	void onBackendConnected() {
+		registerListener();
+		if (getIntent().getAction().equals(ACTION_VIEW_MUC)) {
+			this.uuid = getIntent().getExtras().getString("uuid");
+		}
+		if (uuid != null) {
+			this.conversation = xmppConnectionService
+					.findConversationByUuid(uuid);
+			if (this.conversation != null) {
+				populateView();
+			}
+		}
+	}
+
+	@Override
+	protected void onStop() {
+		if (xmppConnectionServiceBound) {
+			xmppConnectionService.removeOnConversationListChangedListener();
+		}
+		super.onStop();
+	}
+
+	protected void registerListener() {
+		xmppConnectionService
+				.setOnConversationListChangedListener(this.onConvChanged);
+		xmppConnectionService.setOnRenameListener(new OnRenameListener() {
+
+			@Override
+			public void onRename(final boolean success) {
+				runOnUiThread(new Runnable() {
+
+					@Override
+					public void run() {
+						populateView();
+						if (success) {
+							Toast.makeText(
+									ConferenceDetailsActivity.this,
+									getString(R.string.your_nick_has_been_changed),
+									Toast.LENGTH_SHORT).show();
+						} else {
+							Toast.makeText(ConferenceDetailsActivity.this,
+									getString(R.string.nick_in_use),
+									Toast.LENGTH_SHORT).show();
+						}
+					}
+				});
+			}
+		});
+	}
+
+	private void populateView() {
+		mAccountJid.setText(getString(R.string.using_account, conversation
+				.getAccount().getJid()));
+		mYourPhoto.setImageBitmap(avatarService().get(
+				conversation.getAccount(), getPixel(48)));
+		setTitle(conversation.getName());
+		mFullJid.setText(conversation.getContactJid().split("/", 2)[0]);
+		mYourNick.setText(conversation.getMucOptions().getActualNick());
+		mRoleAffiliaton = (TextView) findViewById(R.id.muc_role);
+		if (conversation.getMucOptions().online()) {
+			mMoreDetails.setVisibility(View.VISIBLE);
+			User self = conversation.getMucOptions().getSelf();
+			switch (self.getAffiliation()) {
+			case User.AFFILIATION_ADMIN:
+				mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " ("
+						+ getString(R.string.admin) + ")");
+				break;
+			case User.AFFILIATION_OWNER:
+				mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " ("
+						+ getString(R.string.owner) + ")");
+				break;
+			default:
+				mRoleAffiliaton.setText(getReadableRole(self.getRole()));
+				break;
+			}
+		}
+		this.users.clear();
+		this.users.addAll(conversation.getMucOptions().getUsers());
+		LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+		membersView.removeAllViews();
+		for (final User user : conversation.getMucOptions().getUsers()) {
+			View view = (View) inflater.inflate(R.layout.contact, membersView,
+					false);
+			TextView name = (TextView) view
+					.findViewById(R.id.contact_display_name);
+			TextView key = (TextView) view.findViewById(R.id.key);
+			TextView role = (TextView) view.findViewById(R.id.contact_jid);
+			if (user.getPgpKeyId() != 0) {
+				key.setVisibility(View.VISIBLE);
+				key.setOnClickListener(new OnClickListener() {
+
+					@Override
+					public void onClick(View v) {
+						viewPgpKey(user);
+					}
+				});
+				key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
+			}
+			Bitmap bm;
+			Contact contact = user.getContact();
+			if (contact != null) {
+				bm = avatarService().get(contact, getPixel(48));
+				name.setText(contact.getDisplayName());
+				role.setText(user.getName() + " \u2022 "
+						+ getReadableRole(user.getRole()));
+			} else {
+				bm = avatarService().get(user.getName(), getPixel(48));
+				name.setText(user.getName());
+				role.setText(getReadableRole(user.getRole()));
+			}
+			ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
+			iv.setImageBitmap(bm);
+			membersView.addView(view);
+		}
+	}
+
+	private void viewPgpKey(User user) {
+		PgpEngine pgp = xmppConnectionService.getPgpEngine();
+		if (pgp != null) {
+			PendingIntent intent = pgp.getIntentForKey(
+					conversation.getAccount(), user.getPgpKeyId());
+			if (intent != null) {
+				try {
+					startIntentSenderForResult(intent.getIntentSender(), 0,
+							null, 0, 0, 0);
+				} catch (SendIntentException e) {
+
+				}
+			}
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -0,0 +1,436 @@
+package eu.siacs.conversations.ui;
+
+import java.util.Iterator;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.CheckBox;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.CompoundButton;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class ContactDetailsActivity extends XmppActivity {
+	public static final String ACTION_VIEW_CONTACT = "view_contact";
+
+	private Contact contact;
+
+	private String accountJid;
+	private String contactJid;
+
+	private TextView contactJidTv;
+	private TextView accountJidTv;
+	private TextView status;
+	private TextView lastseen;
+	private CheckBox send;
+	private CheckBox receive;
+	private QuickContactBadge badge;
+
+	private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
+
+		@Override
+		public void onClick(DialogInterface dialog, int which) {
+			ContactDetailsActivity.this.xmppConnectionService
+					.deleteContactOnServer(contact);
+			ContactDetailsActivity.this.finish();
+		}
+	};
+
+	private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
+
+		@Override
+		public void onClick(DialogInterface dialog, int which) {
+			Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+			intent.setType(Contacts.CONTENT_ITEM_TYPE);
+			intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid());
+			intent.putExtra(Intents.Insert.IM_PROTOCOL,
+					CommonDataKinds.Im.PROTOCOL_JABBER);
+			intent.putExtra("finishActivityOnSaveCompleted", true);
+			ContactDetailsActivity.this.startActivityForResult(intent, 0);
+		}
+	};
+	private OnClickListener onBadgeClick = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			AlertDialog.Builder builder = new AlertDialog.Builder(
+					ContactDetailsActivity.this);
+			builder.setTitle(getString(R.string.action_add_phone_book));
+			builder.setMessage(getString(R.string.add_phone_book_text,
+					contact.getJid()));
+			builder.setNegativeButton(getString(R.string.cancel), null);
+			builder.setPositiveButton(getString(R.string.add), addToPhonebook);
+			builder.create().show();
+		}
+	};
+
+	private LinearLayout keys;
+
+	private OnRosterUpdate rosterUpdate = new OnRosterUpdate() {
+
+		@Override
+		public void onRosterUpdate() {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					populateView();
+				}
+			});
+		}
+	};
+
+	private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
+
+		@Override
+		public void onCheckedChanged(CompoundButton buttonView,
+				boolean isChecked) {
+			if (isChecked) {
+				if (contact
+						.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+					xmppConnectionService.sendPresencePacket(contact
+							.getAccount(),
+							xmppConnectionService.getPresenceGenerator()
+									.sendPresenceUpdatesTo(contact));
+				} else {
+					contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+				}
+			} else {
+				contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+				xmppConnectionService.sendPresencePacket(contact.getAccount(),
+						xmppConnectionService.getPresenceGenerator()
+								.stopPresenceUpdatesTo(contact));
+			}
+		}
+	};
+
+	private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
+
+		@Override
+		public void onCheckedChanged(CompoundButton buttonView,
+				boolean isChecked) {
+			if (isChecked) {
+				xmppConnectionService.sendPresencePacket(contact.getAccount(),
+						xmppConnectionService.getPresenceGenerator()
+								.requestPresenceUpdatesFrom(contact));
+			} else {
+				xmppConnectionService.sendPresencePacket(contact.getAccount(),
+						xmppConnectionService.getPresenceGenerator()
+								.stopPresenceUpdatesFrom(contact));
+			}
+		}
+	};
+
+	private OnAccountUpdate accountUpdate = new OnAccountUpdate() {
+
+		@Override
+		public void onAccountUpdate() {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					populateView();
+				}
+			});
+		}
+	};
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
+			this.accountJid = getIntent().getExtras().getString("account");
+			this.contactJid = getIntent().getExtras().getString("contact");
+		}
+		setContentView(R.layout.activity_contact_details);
+
+		contactJidTv = (TextView) findViewById(R.id.details_contactjid);
+		accountJidTv = (TextView) findViewById(R.id.details_account);
+		status = (TextView) findViewById(R.id.details_contactstatus);
+		lastseen = (TextView) findViewById(R.id.details_lastseen);
+		send = (CheckBox) findViewById(R.id.details_send_presence);
+		receive = (CheckBox) findViewById(R.id.details_receive_presence);
+		badge = (QuickContactBadge) findViewById(R.id.details_contact_badge);
+		keys = (LinearLayout) findViewById(R.id.details_contact_keys);
+		getActionBar().setHomeButtonEnabled(true);
+		getActionBar().setDisplayHomeAsUpEnabled(true);
+
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(MenuItem menuItem) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		switch (menuItem.getItemId()) {
+		case android.R.id.home:
+			finish();
+			break;
+		case R.id.action_delete_contact:
+			builder.setTitle(getString(R.string.action_delete_contact))
+					.setMessage(
+							getString(R.string.remove_contact_text,
+									contact.getJid()))
+					.setPositiveButton(getString(R.string.delete),
+							removeFromRoster).create().show();
+			break;
+		case R.id.action_edit_contact:
+			if (contact.getSystemAccount() == null) {
+				quickEdit(contact.getDisplayName(), new OnValueEdited() {
+
+					@Override
+					public void onValueEdited(String value) {
+						contact.setServerName(value);
+						ContactDetailsActivity.this.xmppConnectionService
+								.pushContactToServer(contact);
+						populateView();
+					}
+				});
+			} else {
+				Intent intent = new Intent(Intent.ACTION_EDIT);
+				String[] systemAccount = contact.getSystemAccount().split("#");
+				long id = Long.parseLong(systemAccount[0]);
+				Uri uri = Contacts.getLookupUri(id, systemAccount[1]);
+				intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE);
+				intent.putExtra("finishActivityOnSaveCompleted", true);
+				startActivity(intent);
+			}
+			break;
+		}
+		return super.onOptionsItemSelected(menuItem);
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.contact_details, menu);
+		return true;
+	}
+
+	private void populateView() {
+		send.setOnCheckedChangeListener(null);
+		receive.setOnCheckedChangeListener(null);
+		setTitle(contact.getDisplayName());
+		if (contact.getOption(Contact.Options.FROM)) {
+			send.setText(R.string.send_presence_updates);
+			send.setChecked(true);
+		} else if (contact
+				.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+			send.setChecked(false);
+			send.setText(R.string.send_presence_updates);
+		} else {
+			send.setText(R.string.preemptively_grant);
+			if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
+				send.setChecked(true);
+			} else {
+				send.setChecked(false);
+			}
+		}
+		if (contact.getOption(Contact.Options.TO)) {
+			receive.setText(R.string.receive_presence_updates);
+			receive.setChecked(true);
+		} else {
+			receive.setText(R.string.ask_for_presence_updates);
+			if (contact.getOption(Contact.Options.ASKING)) {
+				receive.setChecked(true);
+			} else {
+				receive.setChecked(false);
+			}
+		}
+		if (contact.getAccount().getStatus() == Account.STATUS_ONLINE) {
+			receive.setEnabled(true);
+			send.setEnabled(true);
+		} else {
+			receive.setEnabled(false);
+			send.setEnabled(false);
+		}
+
+		send.setOnCheckedChangeListener(this.mOnSendCheckedChange);
+		receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
+
+		lastseen.setText(UIHelper.lastseen(getApplicationContext(),
+				contact.lastseen.time));
+
+		switch (contact.getMostAvailableStatus()) {
+		case Presences.CHAT:
+			status.setText(R.string.contact_status_free_to_chat);
+			status.setTextColor(mColorGreen);
+			break;
+		case Presences.ONLINE:
+			status.setText(R.string.contact_status_online);
+			status.setTextColor(mColorGreen);
+			break;
+		case Presences.AWAY:
+			status.setText(R.string.contact_status_away);
+			status.setTextColor(mColorOrange);
+			break;
+		case Presences.XA:
+			status.setText(R.string.contact_status_extended_away);
+			status.setTextColor(mColorOrange);
+			break;
+		case Presences.DND:
+			status.setText(R.string.contact_status_do_not_disturb);
+			status.setTextColor(mColorRed);
+			break;
+		case Presences.OFFLINE:
+			status.setText(R.string.contact_status_offline);
+			status.setTextColor(mSecondaryTextColor);
+			break;
+		default:
+			status.setText(R.string.contact_status_offline);
+			status.setTextColor(mSecondaryTextColor);
+			break;
+		}
+		if (contact.getPresences().size() > 1) {
+			contactJidTv.setText(contact.getJid() + " ("
+					+ contact.getPresences().size() + ")");
+		} else {
+			contactJidTv.setText(contact.getJid());
+		}
+		accountJidTv.setText(getString(R.string.using_account, contact
+				.getAccount().getJid()));
+		prepareContactBadge(badge, contact);
+		if (contact.getSystemAccount() == null) {
+			badge.setOnClickListener(onBadgeClick);
+		}
+
+		keys.removeAllViews();
+		boolean hasKeys = false;
+		LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+		for (Iterator<String> iterator = contact.getOtrFingerprints()
+				.iterator(); iterator.hasNext();) {
+			hasKeys = true;
+			final String otrFingerprint = iterator.next();
+			View view = (View) inflater.inflate(R.layout.contact_key, keys,
+					false);
+			TextView key = (TextView) view.findViewById(R.id.key);
+			TextView keyType = (TextView) view.findViewById(R.id.key_type);
+			ImageButton remove = (ImageButton) view
+					.findViewById(R.id.button_remove);
+			remove.setVisibility(View.VISIBLE);
+			keyType.setText("OTR Fingerprint");
+			key.setText(otrFingerprint);
+			keys.addView(view);
+			remove.setOnClickListener(new OnClickListener() {
+
+				@Override
+				public void onClick(View v) {
+					confirmToDeleteFingerprint(otrFingerprint);
+				}
+			});
+		}
+		if (contact.getPgpKeyId() != 0) {
+			hasKeys = true;
+			View view = (View) inflater.inflate(R.layout.contact_key, keys,
+					false);
+			TextView key = (TextView) view.findViewById(R.id.key);
+			TextView keyType = (TextView) view.findViewById(R.id.key_type);
+			keyType.setText("PGP Key ID");
+			key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
+			view.setOnClickListener(new OnClickListener() {
+
+				@Override
+				public void onClick(View v) {
+					PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService
+							.getPgpEngine();
+					if (pgp != null) {
+						PendingIntent intent = pgp.getIntentForKey(contact);
+						if (intent != null) {
+							try {
+								startIntentSenderForResult(
+										intent.getIntentSender(), 0, null, 0,
+										0, 0);
+							} catch (SendIntentException e) {
+
+							}
+						}
+					}
+				}
+			});
+			keys.addView(view);
+		}
+		if (hasKeys) {
+			keys.setVisibility(View.VISIBLE);
+		} else {
+			keys.setVisibility(View.GONE);
+		}
+	}
+
+	private void prepareContactBadge(QuickContactBadge badge, Contact contact) {
+		if (contact.getSystemAccount() != null) {
+			String[] systemAccount = contact.getSystemAccount().split("#");
+			long id = Long.parseLong(systemAccount[0]);
+			badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1]));
+		}
+		badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
+	}
+
+	protected void confirmToDeleteFingerprint(final String fingerprint) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(R.string.delete_fingerprint);
+		builder.setMessage(R.string.sure_delete_fingerprint);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setPositiveButton(R.string.delete,
+				new android.content.DialogInterface.OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						if (contact.deleteOtrFingerprint(fingerprint)) {
+							populateView();
+							xmppConnectionService.syncRosterToDisk(contact
+									.getAccount());
+						}
+					}
+
+				});
+		builder.create().show();
+	}
+
+	@Override
+	public void onBackendConnected() {
+		xmppConnectionService.setOnRosterUpdateListener(this.rosterUpdate);
+		xmppConnectionService
+				.setOnAccountListChangedListener(this.accountUpdate);
+		if ((accountJid != null) && (contactJid != null)) {
+			Account account = xmppConnectionService
+					.findAccountByJid(accountJid);
+			if (account == null) {
+				return;
+			}
+			this.contact = account.getRoster().getContact(contactJid);
+			populateView();
+		}
+	}
+
+	@Override
+	protected void onStop() {
+		super.onStop();
+		xmppConnectionService.removeOnRosterUpdateListener();
+		xmppConnectionService.removeOnAccountListChangedListener();
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java 🔗

@@ -0,0 +1,947 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.ConversationAdapter;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.app.PendingIntent;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.IntentSender.SendIntentException;
+import android.content.Intent;
+import android.support.v4.widget.SlidingPaneLayout;
+import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.Toast;
+
+public class ConversationActivity extends XmppActivity implements
+		OnAccountUpdate, OnConversationUpdate, OnRosterUpdate {
+
+	public static final String VIEW_CONVERSATION = "viewConversation";
+	public static final String CONVERSATION = "conversationUuid";
+	public static final String TEXT = "text";
+	public static final String PRESENCE = "eu.siacs.conversations.presence";
+
+	public static final int REQUEST_SEND_MESSAGE = 0x0201;
+	public static final int REQUEST_DECRYPT_PGP = 0x0202;
+	private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203;
+	private static final int REQUEST_IMAGE_CAPTURE = 0x0204;
+	private static final int REQUEST_RECORD_AUDIO = 0x0205;
+	private static final int REQUEST_SEND_PGP_IMAGE = 0x0206;
+	public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
+
+	private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
+	private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
+	private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303;
+	private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
+	private static final String STATE_PANEL_OPEN = "state_panel_open";
+
+	private String mOpenConverstaion = null;
+	private boolean mPanelOpen = true;
+
+	private View mContentView;
+
+	private List<Conversation> conversationList = new ArrayList<Conversation>();
+	private Conversation selectedConversation = null;
+	private ListView listView;
+
+	private boolean paneShouldBeOpen = true;
+	private ArrayAdapter<Conversation> listAdapter;
+
+	private Toast prepareImageToast;
+
+	private Uri pendingImageUri = null;
+
+	public List<Conversation> getConversationList() {
+		return this.conversationList;
+	}
+
+	public Conversation getSelectedConversation() {
+		return this.selectedConversation;
+	}
+
+	public void setSelectedConversation(Conversation conversation) {
+		this.selectedConversation = conversation;
+	}
+
+	public ListView getConversationListView() {
+		return this.listView;
+	}
+
+	public boolean shouldPaneBeOpen() {
+		return paneShouldBeOpen;
+	}
+
+	public void showConversationsOverview() {
+		if (mContentView instanceof SlidingPaneLayout) {
+			SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+			mSlidingPaneLayout.openPane();
+		}
+	}
+
+	public void hideConversationsOverview() {
+		if (mContentView instanceof SlidingPaneLayout) {
+			SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+			mSlidingPaneLayout.closePane();
+		}
+	}
+
+	public boolean isConversationsOverviewHideable() {
+		if (mContentView instanceof SlidingPaneLayout) {
+			SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+			return mSlidingPaneLayout.isSlideable();
+		} else {
+			return false;
+		}
+	}
+
+	public boolean isConversationsOverviewVisable() {
+		if (mContentView instanceof SlidingPaneLayout) {
+			SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+			return mSlidingPaneLayout.isOpen();
+		} else {
+			return true;
+		}
+	}
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		if (savedInstanceState != null) {
+			mOpenConverstaion = savedInstanceState.getString(
+					STATE_OPEN_CONVERSATION, null);
+			mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true);
+		}
+
+		setContentView(R.layout.fragment_conversations_overview);
+
+		listView = (ListView) findViewById(R.id.list);
+
+		getActionBar().setDisplayHomeAsUpEnabled(false);
+		getActionBar().setHomeButtonEnabled(false);
+
+		this.listAdapter = new ConversationAdapter(this, conversationList);
+		listView.setAdapter(this.listAdapter);
+
+		listView.setOnItemClickListener(new OnItemClickListener() {
+
+			@Override
+			public void onItemClick(AdapterView<?> arg0, View clickedView,
+					int position, long arg3) {
+				paneShouldBeOpen = false;
+				if (getSelectedConversation() != conversationList.get(position)) {
+					setSelectedConversation(conversationList.get(position));
+					swapConversationFragment();
+				} else {
+					hideConversationsOverview();
+				}
+			}
+		});
+		mContentView = findViewById(R.id.content_view_spl);
+		if (mContentView == null) {
+			mContentView = findViewById(R.id.content_view_ll);
+		}
+		if (mContentView instanceof SlidingPaneLayout) {
+			SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
+			mSlidingPaneLayout.setParallaxDistance(150);
+			mSlidingPaneLayout
+					.setShadowResource(R.drawable.es_slidingpane_shadow);
+			mSlidingPaneLayout.setSliderFadeColor(0);
+			mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
+
+				@Override
+				public void onPanelOpened(View arg0) {
+					paneShouldBeOpen = true;
+					ActionBar ab = getActionBar();
+					if (ab != null) {
+						ab.setDisplayHomeAsUpEnabled(false);
+						ab.setHomeButtonEnabled(false);
+						ab.setTitle(R.string.app_name);
+					}
+					invalidateOptionsMenu();
+					hideKeyboard();
+					if (xmppConnectionServiceBound) {
+						xmppConnectionService.getNotificationService()
+								.setOpenConversation(null);
+					}
+				}
+
+				@Override
+				public void onPanelClosed(View arg0) {
+					paneShouldBeOpen = false;
+					if ((conversationList.size() > 0)
+							&& (getSelectedConversation() != null)) {
+						openConversation(getSelectedConversation());
+						if (!getSelectedConversation().isRead()) {
+							xmppConnectionService.markRead(
+									getSelectedConversation(), true);
+							listView.invalidateViews();
+						}
+					}
+				}
+
+				@Override
+				public void onPanelSlide(View arg0, float arg1) {
+					// TODO Auto-generated method stub
+
+				}
+			});
+		}
+	}
+
+	public void openConversation(Conversation conversation) {
+		ActionBar ab = getActionBar();
+		if (ab != null) {
+			ab.setDisplayHomeAsUpEnabled(true);
+			ab.setHomeButtonEnabled(true);
+			if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE
+					|| ConversationActivity.this
+							.useSubjectToIdentifyConference()) {
+				ab.setTitle(getSelectedConversation().getName());
+			} else {
+				ab.setTitle(getSelectedConversation().getContactJid()
+						.split("/")[0]);
+			}
+		}
+		invalidateOptionsMenu();
+		if (xmppConnectionServiceBound) {
+			xmppConnectionService.getNotificationService().setOpenConversation(
+					conversation);
+		}
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.conversations, menu);
+		MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security);
+		MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive);
+		MenuItem menuMucDetails = (MenuItem) menu
+				.findItem(R.id.action_muc_details);
+		MenuItem menuContactDetails = (MenuItem) menu
+				.findItem(R.id.action_contact_details);
+		MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file);
+		MenuItem menuClearHistory = (MenuItem) menu
+				.findItem(R.id.action_clear_history);
+		MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add);
+		MenuItem menuInviteContact = (MenuItem) menu
+				.findItem(R.id.action_invite);
+		MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute);
+
+		if (isConversationsOverviewVisable()
+				&& isConversationsOverviewHideable()) {
+			menuArchive.setVisible(false);
+			menuMucDetails.setVisible(false);
+			menuContactDetails.setVisible(false);
+			menuSecure.setVisible(false);
+			menuInviteContact.setVisible(false);
+			menuAttach.setVisible(false);
+			menuClearHistory.setVisible(false);
+			menuMute.setVisible(false);
+		} else {
+			menuAdd.setVisible(!isConversationsOverviewHideable());
+			if (this.getSelectedConversation() != null) {
+				if (this.getSelectedConversation().getLatestMessage()
+						.getEncryption() != Message.ENCRYPTION_NONE) {
+					menuSecure.setIcon(R.drawable.ic_action_secure);
+				}
+				if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) {
+					menuContactDetails.setVisible(false);
+					menuAttach.setVisible(false);
+				} else {
+					menuMucDetails.setVisible(false);
+					menuInviteContact.setVisible(false);
+				}
+			}
+		}
+		return true;
+	}
+
+	private void selectPresenceToAttachFile(final int attachmentChoice) {
+		selectPresence(getSelectedConversation(), new OnPresenceSelected() {
+
+			@Override
+			public void onPresenceSelected() {
+				if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+					pendingImageUri = xmppConnectionService.getFileBackend()
+							.getTakePhotoUri();
+					Intent takePictureIntent = new Intent(
+							MediaStore.ACTION_IMAGE_CAPTURE);
+					takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
+							pendingImageUri);
+					if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
+						startActivityForResult(takePictureIntent,
+								REQUEST_IMAGE_CAPTURE);
+					}
+				} else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+					Intent attachFileIntent = new Intent();
+					attachFileIntent.setType("image/*");
+					attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
+					Intent chooser = Intent.createChooser(attachFileIntent,
+							getString(R.string.attach_file));
+					startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG);
+				} else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
+					Intent intent = new Intent(
+							MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+					startActivityForResult(intent, REQUEST_RECORD_AUDIO);
+				}
+			}
+		});
+	}
+
+	private void attachFile(final int attachmentChoice) {
+		final Conversation conversation = getSelectedConversation();
+		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+			if (hasPgp()) {
+				if (conversation.getContact().getPgpKeyId() != 0) {
+					xmppConnectionService.getPgpEngine().hasKey(
+							conversation.getContact(),
+							new UiCallback<Contact>() {
+
+								@Override
+								public void userInputRequried(PendingIntent pi,
+										Contact contact) {
+									ConversationActivity.this.runIntent(pi,
+											attachmentChoice);
+								}
+
+								@Override
+								public void success(Contact contact) {
+									selectPresenceToAttachFile(attachmentChoice);
+								}
+
+								@Override
+								public void error(int error, Contact contact) {
+									displayErrorDialog(error);
+								}
+							});
+				} else {
+					final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+							.findFragmentByTag("conversation");
+					if (fragment != null) {
+						fragment.showNoPGPKeyDialog(false,
+								new OnClickListener() {
+
+									@Override
+									public void onClick(DialogInterface dialog,
+											int which) {
+										conversation
+												.setNextEncryption(Message.ENCRYPTION_NONE);
+										xmppConnectionService.databaseBackend
+												.updateConversation(conversation);
+										selectPresenceToAttachFile(attachmentChoice);
+									}
+								});
+					}
+				}
+			} else {
+				showInstallPgpDialog();
+			}
+		} else if (getSelectedConversation().getNextEncryption(
+				forceEncryption()) == Message.ENCRYPTION_NONE) {
+			selectPresenceToAttachFile(attachmentChoice);
+		} else {
+			selectPresenceToAttachFile(attachmentChoice);
+		}
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(MenuItem item) {
+		if (item.getItemId() == android.R.id.home) {
+			showConversationsOverview();
+			return true;
+		} else if (item.getItemId() == R.id.action_add) {
+			startActivity(new Intent(this, StartConversationActivity.class));
+			return true;
+		} else if (getSelectedConversation() != null) {
+			switch (item.getItemId()) {
+			case R.id.action_attach_file:
+				attachFileDialog();
+				break;
+			case R.id.action_archive:
+				this.endConversation(getSelectedConversation());
+				break;
+			case R.id.action_contact_details:
+				Contact contact = this.getSelectedConversation().getContact();
+				if (contact.showInRoster()) {
+					switchToContactDetails(contact);
+				} else {
+					showAddToRosterDialog(getSelectedConversation());
+				}
+				break;
+			case R.id.action_muc_details:
+				Intent intent = new Intent(this,
+						ConferenceDetailsActivity.class);
+				intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
+				intent.putExtra("uuid", getSelectedConversation().getUuid());
+				startActivity(intent);
+				break;
+			case R.id.action_invite:
+				inviteToConversation(getSelectedConversation());
+				break;
+			case R.id.action_security:
+				selectEncryptionDialog(getSelectedConversation());
+				break;
+			case R.id.action_clear_history:
+				clearHistoryDialog(getSelectedConversation());
+				break;
+			case R.id.action_mute:
+				muteConversationDialog(getSelectedConversation());
+				break;
+			default:
+				break;
+			}
+			return super.onOptionsItemSelected(item);
+		} else {
+			return super.onOptionsItemSelected(item);
+		}
+	}
+
+	public void endConversation(Conversation conversation) {
+		conversation.setStatus(Conversation.STATUS_ARCHIVED);
+		paneShouldBeOpen = true;
+		showConversationsOverview();
+		xmppConnectionService.archiveConversation(conversation);
+		if (conversationList.size() > 0) {
+			setSelectedConversation(conversationList.get(0));
+		} else {
+			setSelectedConversation(null);
+		}
+	}
+
+	@SuppressLint("InflateParams")
+	protected void clearHistoryDialog(final Conversation conversation) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(getString(R.string.clear_conversation_history));
+		View dialogView = getLayoutInflater().inflate(
+				R.layout.dialog_clear_history, null);
+		final CheckBox endConversationCheckBox = (CheckBox) dialogView
+				.findViewById(R.id.end_conversation_checkbox);
+		builder.setView(dialogView);
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		builder.setPositiveButton(getString(R.string.delete_messages),
+				new OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						ConversationActivity.this.xmppConnectionService
+								.clearConversationHistory(conversation);
+						if (endConversationCheckBox.isChecked()) {
+							endConversation(conversation);
+						}
+					}
+				});
+		builder.create().show();
+	}
+
+	protected void attachFileDialog() {
+		View menuAttachFile = findViewById(R.id.action_attach_file);
+		if (menuAttachFile == null) {
+			return;
+		}
+		PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile);
+		attachFilePopup.inflate(R.menu.attachment_choices);
+		attachFilePopup
+				.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+					@Override
+					public boolean onMenuItemClick(MenuItem item) {
+						switch (item.getItemId()) {
+						case R.id.attach_choose_picture:
+							attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+							break;
+						case R.id.attach_take_picture:
+							attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+							break;
+						case R.id.attach_record_voice:
+							attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
+							break;
+						}
+						return false;
+					}
+				});
+		attachFilePopup.show();
+	}
+
+	protected void selectEncryptionDialog(final Conversation conversation) {
+		View menuItemView = findViewById(R.id.action_security);
+		if (menuItemView == null) {
+			return;
+		}
+		PopupMenu popup = new PopupMenu(this, menuItemView);
+		final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+				.findFragmentByTag("conversation");
+		if (fragment != null) {
+			popup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+				@Override
+				public boolean onMenuItemClick(MenuItem item) {
+					switch (item.getItemId()) {
+					case R.id.encryption_choice_none:
+						conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+						item.setChecked(true);
+						break;
+					case R.id.encryption_choice_otr:
+						conversation.setNextEncryption(Message.ENCRYPTION_OTR);
+						item.setChecked(true);
+						break;
+					case R.id.encryption_choice_pgp:
+						if (hasPgp()) {
+							if (conversation.getAccount().getKeys()
+									.has("pgp_signature")) {
+								conversation
+										.setNextEncryption(Message.ENCRYPTION_PGP);
+								item.setChecked(true);
+							} else {
+								announcePgp(conversation.getAccount(),
+										conversation);
+							}
+						} else {
+							showInstallPgpDialog();
+						}
+						break;
+					default:
+						conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+						break;
+					}
+					xmppConnectionService.databaseBackend
+							.updateConversation(conversation);
+					fragment.updateChatMsgHint();
+					return true;
+				}
+			});
+			popup.inflate(R.menu.encryption_choices);
+			MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
+			MenuItem none = popup.getMenu().findItem(
+					R.id.encryption_choice_none);
+			if (conversation.getMode() == Conversation.MODE_MULTI) {
+				otr.setEnabled(false);
+			} else {
+				if (forceEncryption()) {
+					none.setVisible(false);
+				}
+			}
+			switch (conversation.getNextEncryption(forceEncryption())) {
+			case Message.ENCRYPTION_NONE:
+				none.setChecked(true);
+				break;
+			case Message.ENCRYPTION_OTR:
+				otr.setChecked(true);
+				break;
+			case Message.ENCRYPTION_PGP:
+				popup.getMenu().findItem(R.id.encryption_choice_pgp)
+						.setChecked(true);
+				break;
+			default:
+				popup.getMenu().findItem(R.id.encryption_choice_none)
+						.setChecked(true);
+				break;
+			}
+			popup.show();
+		}
+	}
+
+	protected void muteConversationDialog(final Conversation conversation) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(R.string.disable_notifications_for_this_conversation);
+		final int[] durations = getResources().getIntArray(
+				R.array.mute_options_durations);
+		builder.setItems(R.array.mute_options_descriptions,
+				new OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						long till;
+						if (durations[which] == -1) {
+							till = Long.MAX_VALUE;
+						} else {
+							till = SystemClock.elapsedRealtime()
+									+ (durations[which] * 1000);
+						}
+						conversation.setMutedTill(till);
+						ConversationActivity.this.xmppConnectionService.databaseBackend
+								.updateConversation(conversation);
+						ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+								.findFragmentByTag("conversation");
+						if (selectedFragment != null) {
+							selectedFragment.updateMessages();
+						}
+					}
+				});
+		builder.create().show();
+	}
+
+	protected ConversationFragment swapConversationFragment() {
+		ConversationFragment selectedFragment = new ConversationFragment();
+		if (!isFinishing()) {
+
+			FragmentTransaction transaction = getFragmentManager()
+					.beginTransaction();
+			transaction.replace(R.id.selected_conversation, selectedFragment,
+					"conversation");
+			try {
+				transaction.commitAllowingStateLoss();
+			} catch (IllegalStateException e) {
+				return selectedFragment;
+			}
+		}
+		return selectedFragment;
+	}
+
+	@Override
+	public boolean onKeyDown(int keyCode, KeyEvent event) {
+		if (keyCode == KeyEvent.KEYCODE_BACK) {
+			if (!isConversationsOverviewVisable()) {
+				showConversationsOverview();
+				return false;
+			}
+		}
+		return super.onKeyDown(keyCode, event);
+	}
+
+	@Override
+	protected void onNewIntent(Intent intent) {
+		if (xmppConnectionServiceBound) {
+			if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION
+					.equals(intent.getType())))) {
+				String convToView = (String) intent.getExtras().get(
+						CONVERSATION);
+				updateConversationList();
+				for (int i = 0; i < conversationList.size(); ++i) {
+					if (conversationList.get(i).getUuid().equals(convToView)) {
+						setSelectedConversation(conversationList.get(i));
+						break;
+					}
+				}
+				paneShouldBeOpen = false;
+				String text = intent.getExtras().getString(TEXT, null);
+				swapConversationFragment().setText(text);
+			}
+		} else {
+			handledViewIntent = false;
+			setIntent(intent);
+		}
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		if (this.xmppConnectionServiceBound) {
+			this.onBackendConnected();
+		}
+		if (conversationList.size() >= 1) {
+			this.onConversationUpdate();
+		}
+	}
+
+	@Override
+	protected void onStop() {
+		if (xmppConnectionServiceBound) {
+			xmppConnectionService.removeOnConversationListChangedListener();
+			xmppConnectionService.removeOnAccountListChangedListener();
+			xmppConnectionService.removeOnRosterUpdateListener();
+			xmppConnectionService.getNotificationService().setOpenConversation(
+					null);
+		}
+		super.onStop();
+	}
+
+	@Override
+	public void onSaveInstanceState(Bundle savedInstanceState) {
+		Conversation conversation = getSelectedConversation();
+		if (conversation != null) {
+			savedInstanceState.putString(STATE_OPEN_CONVERSATION,
+					conversation.getUuid());
+		}
+		savedInstanceState.putBoolean(STATE_PANEL_OPEN,
+				isConversationsOverviewVisable());
+		super.onSaveInstanceState(savedInstanceState);
+	}
+
+	@Override
+	void onBackendConnected() {
+		this.registerListener();
+		updateConversationList();
+
+		if (xmppConnectionService.getAccounts().size() == 0) {
+			startActivity(new Intent(this, EditAccountActivity.class));
+		} else if (conversationList.size() <= 0) {
+			startActivity(new Intent(this, StartConversationActivity.class));
+			finish();
+		} else if (mOpenConverstaion != null) {
+			selectConversationByUuid(mOpenConverstaion);
+			paneShouldBeOpen = mPanelOpen;
+			if (paneShouldBeOpen) {
+				showConversationsOverview();
+			}
+			swapConversationFragment();
+			mOpenConverstaion = null;
+		} else if (getIntent() != null
+				&& VIEW_CONVERSATION.equals(getIntent().getType())) {
+			String uuid = (String) getIntent().getExtras().get(CONVERSATION);
+			String text = getIntent().getExtras().getString(TEXT, null);
+			selectConversationByUuid(uuid);
+			paneShouldBeOpen = false;
+			swapConversationFragment().setText(text);
+			setIntent(null);
+		} else {
+			showConversationsOverview();
+			ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+					.findFragmentByTag("conversation");
+			if (selectedFragment != null) {
+				selectedFragment.onBackendConnected();
+			} else {
+				pendingImageUri = null;
+				setSelectedConversation(conversationList.get(0));
+				swapConversationFragment();
+			}
+		}
+
+		if (pendingImageUri != null) {
+			attachImageToConversation(getSelectedConversation(),
+					pendingImageUri);
+			pendingImageUri = null;
+		}
+		ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
+	}
+
+	private void selectConversationByUuid(String uuid) {
+		for (int i = 0; i < conversationList.size(); ++i) {
+			if (conversationList.get(i).getUuid().equals(uuid)) {
+				setSelectedConversation(conversationList.get(i));
+			}
+		}
+	}
+
+	public void registerListener() {
+		xmppConnectionService.setOnConversationListChangedListener(this);
+		xmppConnectionService.setOnAccountListChangedListener(this);
+		xmppConnectionService.setOnRosterUpdateListener(this);
+	}
+
+	@Override
+	protected void onActivityResult(int requestCode, int resultCode,
+			final Intent data) {
+		super.onActivityResult(requestCode, resultCode, data);
+		if (resultCode == RESULT_OK) {
+			if (requestCode == REQUEST_DECRYPT_PGP) {
+				ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+						.findFragmentByTag("conversation");
+				if (selectedFragment != null) {
+					selectedFragment.hideSnackbar();
+					selectedFragment.updateMessages();
+				}
+			} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
+				pendingImageUri = data.getData();
+				if (xmppConnectionServiceBound) {
+					attachImageToConversation(getSelectedConversation(),
+							pendingImageUri);
+					pendingImageUri = null;
+				}
+			} else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
+
+			} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+				attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+			} else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+				attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+			} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
+				announcePgp(getSelectedConversation().getAccount(),
+						getSelectedConversation());
+			} else if (requestCode == REQUEST_ENCRYPT_MESSAGE) {
+				// encryptTextMessage();
+			} else if (requestCode == REQUEST_IMAGE_CAPTURE) {
+				if (xmppConnectionServiceBound) {
+					attachImageToConversation(getSelectedConversation(),
+							pendingImageUri);
+					pendingImageUri = null;
+				}
+				Intent intent = new Intent(
+						Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+				intent.setData(pendingImageUri);
+				sendBroadcast(intent);
+			} else if (requestCode == REQUEST_RECORD_AUDIO) {
+				attachAudioToConversation(getSelectedConversation(),
+						data.getData());
+			}
+		} else {
+			if (requestCode == REQUEST_IMAGE_CAPTURE) {
+				pendingImageUri = null;
+			}
+		}
+	}
+
+	private void attachAudioToConversation(Conversation conversation, Uri uri) {
+
+	}
+
+	private void attachImageToConversation(Conversation conversation, Uri uri) {
+		prepareImageToast = Toast.makeText(getApplicationContext(),
+				getText(R.string.preparing_image), Toast.LENGTH_LONG);
+		prepareImageToast.show();
+		xmppConnectionService.attachImageToConversation(conversation, uri,
+				new UiCallback<Message>() {
+
+					@Override
+					public void userInputRequried(PendingIntent pi,
+							Message object) {
+						hidePrepareImageToast();
+						ConversationActivity.this.runIntent(pi,
+								ConversationActivity.REQUEST_SEND_PGP_IMAGE);
+					}
+
+					@Override
+					public void success(Message message) {
+						xmppConnectionService.sendMessage(message);
+					}
+
+					@Override
+					public void error(int error, Message message) {
+						hidePrepareImageToast();
+						displayErrorDialog(error);
+					}
+				});
+	}
+
+	private void hidePrepareImageToast() {
+		if (prepareImageToast != null) {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					prepareImageToast.cancel();
+				}
+			});
+		}
+	}
+
+	public void updateConversationList() {
+		xmppConnectionService
+				.populateWithOrderedConversations(conversationList);
+		listAdapter.notifyDataSetChanged();
+	}
+
+	public void runIntent(PendingIntent pi, int requestCode) {
+		try {
+			this.startIntentSenderForResult(pi.getIntentSender(), requestCode,
+					null, 0, 0, 0);
+		} catch (SendIntentException e1) {
+		}
+	}
+
+	public void encryptTextMessage(Message message) {
+		xmppConnectionService.getPgpEngine().encrypt(message,
+				new UiCallback<Message>() {
+
+					@Override
+					public void userInputRequried(PendingIntent pi,
+							Message message) {
+						ConversationActivity.this.runIntent(pi,
+								ConversationActivity.REQUEST_SEND_MESSAGE);
+					}
+
+					@Override
+					public void success(Message message) {
+						message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+						xmppConnectionService.sendMessage(message);
+					}
+
+					@Override
+					public void error(int error, Message message) {
+
+					}
+				});
+	}
+
+	public boolean forceEncryption() {
+		return getPreferences().getBoolean("force_encryption", false);
+	}
+
+	public boolean useSendButtonToIndicateStatus() {
+		return getPreferences().getBoolean("send_button_status", false);
+	}
+
+	public boolean indicateReceived() {
+		return getPreferences().getBoolean("indicate_received", false);
+	}
+
+	@Override
+	public void onAccountUpdate() {
+		final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+				.findFragmentByTag("conversation");
+		if (fragment != null) {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					fragment.updateMessages();
+				}
+			});
+		}
+	}
+
+	@Override
+	public void onConversationUpdate() {
+		runOnUiThread(new Runnable() {
+
+			@Override
+			public void run() {
+				updateConversationList();
+				if (paneShouldBeOpen) {
+					if (conversationList.size() >= 1) {
+						swapConversationFragment();
+					} else {
+						startActivity(new Intent(getApplicationContext(),
+								StartConversationActivity.class));
+						finish();
+					}
+				}
+				ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+						.findFragmentByTag("conversation");
+				if (selectedFragment != null) {
+					selectedFragment.updateMessages();
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onRosterUpdate() {
+		final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+				.findFragmentByTag("conversation");
+		if (fragment != null) {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					fragment.updateMessages();
+				}
+			});
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -0,0 +1,781 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.EditMessage.OnEnterPressed;
+import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
+import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
+import eu.siacs.conversations.ui.adapter.MessageAdapter;
+import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
+import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
+import eu.siacs.conversations.utils.UIHelper;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Selection;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.AbsListView;
+
+import android.widget.ListView;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class ConversationFragment extends Fragment {
+
+	protected Conversation conversation;
+	protected ListView messagesView;
+	protected LayoutInflater inflater;
+	protected List<Message> messageList = new ArrayList<Message>();
+	protected MessageAdapter messageListAdapter;
+	protected Contact contact;
+
+	protected String queuedPqpMessage = null;
+
+	private EditMessage mEditMessage;
+	private ImageButton mSendButton;
+	private String pastedText = null;
+	private RelativeLayout snackbar;
+	private TextView snackbarMessage;
+	private TextView snackbarAction;
+
+	private boolean messagesLoaded = false;
+
+	private IntentSender askForPassphraseIntent = null;
+
+	private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>();
+	private boolean mDecryptJobRunning = false;
+
+	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
+
+		@Override
+		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+			if (actionId == EditorInfo.IME_ACTION_SEND) {
+				InputMethodManager imm = (InputMethodManager) v.getContext()
+						.getSystemService(Context.INPUT_METHOD_SERVICE);
+				imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+				sendMessage();
+				return true;
+			} else {
+				return false;
+			}
+		}
+	};
+
+	private OnClickListener mSendButtonListener = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			sendMessage();
+		}
+	};
+	protected OnClickListener clickToDecryptListener = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			if (activity.hasPgp() && askForPassphraseIntent != null) {
+				try {
+					getActivity().startIntentSenderForResult(
+							askForPassphraseIntent,
+							ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
+							0, 0);
+				} catch (SendIntentException e) {
+					//
+				}
+			}
+		}
+	};
+
+	private OnClickListener clickToMuc = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			Intent intent = new Intent(getActivity(),
+					ConferenceDetailsActivity.class);
+			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
+			intent.putExtra("uuid", conversation.getUuid());
+			startActivity(intent);
+		}
+	};
+
+	private OnClickListener leaveMuc = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			activity.endConversation(conversation);
+		}
+	};
+
+	private OnClickListener joinMuc = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			activity.xmppConnectionService.joinMuc(conversation);
+		}
+	};
+
+	private OnClickListener enterPassword = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			MucOptions muc = conversation.getMucOptions();
+			String password = muc.getPassword();
+			if (password == null) {
+				password = "";
+			}
+			activity.quickPasswordEdit(password, new OnValueEdited() {
+
+				@Override
+				public void onValueEdited(String value) {
+					activity.xmppConnectionService.providePasswordForMuc(
+							conversation, value);
+				}
+			});
+		}
+	};
+
+	private OnScrollListener mOnScrollListener = new OnScrollListener() {
+
+		@Override
+		public void onScrollStateChanged(AbsListView view, int scrollState) {
+			// TODO Auto-generated method stub
+
+		}
+
+		@Override
+		public void onScroll(AbsListView view, int firstVisibleItem,
+				int visibleItemCount, int totalItemCount) {
+			if (firstVisibleItem == 0 && messagesLoaded) {
+				long timestamp = messageList.get(0).getTimeSent();
+				messagesLoaded = false;
+				int size = activity.xmppConnectionService.loadMoreMessages(
+						conversation, timestamp);
+				messageList.clear();
+				messageList.addAll(conversation.getMessages());
+				updateStatusMessages();
+				messageListAdapter.notifyDataSetChanged();
+				if (size != 0) {
+					messagesLoaded = true;
+				}
+				messagesView.setSelectionFromTop(size + 1, 0);
+			}
+		}
+	};
+
+	private ConversationActivity activity;
+
+	private void sendMessage() {
+		if (this.conversation == null) {
+			return;
+		}
+		if (mEditMessage.getText().length() < 1) {
+			if (this.conversation.getMode() == Conversation.MODE_MULTI) {
+				conversation.setNextPresence(null);
+				updateChatMsgHint();
+			}
+			return;
+		}
+		Message message = new Message(conversation, mEditMessage.getText()
+				.toString(), conversation.getNextEncryption(activity
+				.forceEncryption()));
+		if (conversation.getMode() == Conversation.MODE_MULTI) {
+			if (conversation.getNextPresence() != null) {
+				message.setPresence(conversation.getNextPresence());
+				message.setType(Message.TYPE_PRIVATE);
+				conversation.setNextPresence(null);
+			}
+		}
+		if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
+			sendOtrMessage(message);
+		} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
+			sendPgpMessage(message);
+		} else {
+			sendPlainTextMessage(message);
+		}
+	}
+
+	public void updateChatMsgHint() {
+		if (conversation.getMode() == Conversation.MODE_MULTI
+				&& conversation.getNextPresence() != null) {
+			this.mEditMessage.setHint(getString(
+					R.string.send_private_message_to,
+					conversation.getNextPresence()));
+		} else {
+			switch (conversation.getNextEncryption(activity.forceEncryption())) {
+			case Message.ENCRYPTION_NONE:
+				mEditMessage
+						.setHint(getString(R.string.send_plain_text_message));
+				break;
+			case Message.ENCRYPTION_OTR:
+				mEditMessage.setHint(getString(R.string.send_otr_message));
+				break;
+			case Message.ENCRYPTION_PGP:
+				mEditMessage.setHint(getString(R.string.send_pgp_message));
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	@Override
+	public View onCreateView(final LayoutInflater inflater,
+			ViewGroup container, Bundle savedInstanceState) {
+		final View view = inflater.inflate(R.layout.fragment_conversation,
+				container, false);
+		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
+		mEditMessage.setOnClickListener(new OnClickListener() {
+
+			@Override
+			public void onClick(View v) {
+				activity.hideConversationsOverview();
+			}
+		});
+		mEditMessage.setOnEditorActionListener(mEditorActionListener);
+		mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
+
+			@Override
+			public void onEnterPressed() {
+				sendMessage();
+			}
+		});
+
+		mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
+		mSendButton.setOnClickListener(this.mSendButtonListener);
+
+		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
+		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
+		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
+
+		messagesView = (ListView) view.findViewById(R.id.messages_view);
+		messagesView.setOnScrollListener(mOnScrollListener);
+		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
+		messageListAdapter = new MessageAdapter(
+				(ConversationActivity) getActivity(), this.messageList);
+		messageListAdapter
+				.setOnContactPictureClicked(new OnContactPictureClicked() {
+
+					@Override
+					public void onContactPictureClicked(Message message) {
+						if (message.getStatus() <= Message.STATUS_RECEIVED) {
+							if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+								if (message.getPresence() != null) {
+									highlightInConference(message.getPresence());
+								} else {
+									highlightInConference(message
+											.getCounterpart());
+								}
+							} else {
+								Contact contact = message.getConversation()
+										.getContact();
+								if (contact.showInRoster()) {
+									activity.switchToContactDetails(contact);
+								} else {
+									activity.showAddToRosterDialog(message
+											.getConversation());
+								}
+							}
+						}
+					}
+				});
+		messageListAdapter
+				.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
+
+					@Override
+					public void onContactPictureLongClicked(Message message) {
+						if (message.getStatus() <= Message.STATUS_RECEIVED) {
+							if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+								if (message.getPresence() != null) {
+									privateMessageWith(message.getPresence());
+								} else {
+									privateMessageWith(message.getCounterpart());
+								}
+							}
+						}
+					}
+				});
+		messagesView.setAdapter(messageListAdapter);
+
+		return view;
+	}
+
+	protected void privateMessageWith(String counterpart) {
+		this.mEditMessage.setText("");
+		this.conversation.setNextPresence(counterpart);
+		updateChatMsgHint();
+	}
+
+	protected void highlightInConference(String nick) {
+		String oldString = mEditMessage.getText().toString().trim();
+		if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
+			mEditMessage.getText().insert(0, nick + ": ");
+		} else {
+			if (mEditMessage.getText().charAt(
+					mEditMessage.getSelectionStart() - 1) != ' ') {
+				nick = " " + nick;
+			}
+			mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
+					nick + " ");
+		}
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		this.activity = (ConversationActivity) getActivity();
+		if (activity.xmppConnectionServiceBound) {
+			this.onBackendConnected();
+		}
+	}
+
+	@Override
+	public void onStop() {
+		mDecryptJobRunning = false;
+		super.onStop();
+		if (this.conversation != null) {
+			this.conversation.setNextMessage(mEditMessage.getText().toString());
+		}
+	}
+
+	public void onBackendConnected() {
+		this.activity = (ConversationActivity) getActivity();
+		this.conversation = activity.getSelectedConversation();
+		if (this.conversation == null) {
+			return;
+		}
+		String oldString = conversation.getNextMessage().trim();
+		if (this.pastedText == null) {
+			this.mEditMessage.setText(oldString);
+		} else {
+
+			if (oldString.isEmpty()) {
+				mEditMessage.setText(pastedText);
+			} else {
+				mEditMessage.setText(oldString + " " + pastedText);
+			}
+			pastedText = null;
+		}
+		int position = mEditMessage.length();
+		Editable etext = mEditMessage.getText();
+		Selection.setSelection(etext, position);
+		if (activity.isConversationsOverviewHideable()) {
+			if (!activity.shouldPaneBeOpen()) {
+				activity.hideConversationsOverview();
+				activity.openConversation(conversation);
+			}
+		}
+		if (this.conversation.getMode() == Conversation.MODE_MULTI) {
+			conversation.setNextPresence(null);
+		}
+		updateMessages();
+	}
+
+	public void updateMessages() {
+		if (getView() == null) {
+			return;
+		}
+		hideSnackbar();
+		final ConversationActivity activity = (ConversationActivity) getActivity();
+		if (this.conversation != null) {
+			final Contact contact = this.conversation.getContact();
+			if (this.conversation.isMuted()) {
+				showSnackbar(R.string.notifications_disabled, R.string.enable,
+						new OnClickListener() {
+
+							@Override
+							public void onClick(View v) {
+								conversation.setMutedTill(0);
+								activity.xmppConnectionService.databaseBackend
+										.updateConversation(conversation);
+								updateMessages();
+							}
+						});
+			} else if (!contact.showInRoster()
+					&& contact
+							.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+				showSnackbar(R.string.contact_added_you, R.string.add_back,
+						new OnClickListener() {
+
+							@Override
+							public void onClick(View v) {
+								activity.xmppConnectionService
+										.createContact(contact);
+								activity.switchToContactDetails(contact);
+							}
+						});
+			}
+			for (Message message : this.conversation.getMessages()) {
+				if ((message.getEncryption() == Message.ENCRYPTION_PGP)
+						&& ((message.getStatus() == Message.STATUS_RECEIVED) || (message
+								.getStatus() == Message.STATUS_SEND))) {
+					if (!mEncryptedMessages.contains(message)) {
+						mEncryptedMessages.add(message);
+					}
+				}
+			}
+			decryptNext();
+			this.messageList.clear();
+			if (this.conversation.getMessages().size() == 0) {
+				messagesLoaded = false;
+			} else {
+				this.messageList.addAll(this.conversation.getMessages());
+				messagesLoaded = true;
+				updateStatusMessages();
+			}
+			this.messageListAdapter.notifyDataSetChanged();
+			if (conversation.getMode() == Conversation.MODE_SINGLE) {
+				if (messageList.size() >= 1) {
+					makeFingerprintWarning(conversation.getLatestEncryption());
+				}
+			} else {
+				if (!conversation.getMucOptions().online()
+						&& conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
+					int error = conversation.getMucOptions().getError();
+					switch (error) {
+					case MucOptions.ERROR_NICK_IN_USE:
+						showSnackbar(R.string.nick_in_use, R.string.edit,
+								clickToMuc);
+						break;
+					case MucOptions.ERROR_ROOM_NOT_FOUND:
+						showSnackbar(R.string.conference_not_found,
+								R.string.leave, leaveMuc);
+						break;
+					case MucOptions.ERROR_PASSWORD_REQUIRED:
+						showSnackbar(R.string.conference_requires_password,
+								R.string.enter_password, enterPassword);
+						break;
+					case MucOptions.ERROR_BANNED:
+						showSnackbar(R.string.conference_banned,
+								R.string.leave, leaveMuc);
+						break;
+					case MucOptions.ERROR_MEMBERS_ONLY:
+						showSnackbar(R.string.conference_members_only,
+								R.string.leave, leaveMuc);
+						break;
+					case MucOptions.KICKED_FROM_ROOM:
+						showSnackbar(R.string.conference_kicked, R.string.join,
+								joinMuc);
+						break;
+					default:
+						break;
+					}
+				}
+			}
+			getActivity().invalidateOptionsMenu();
+			updateChatMsgHint();
+			if (!activity.shouldPaneBeOpen()) {
+				activity.xmppConnectionService.markRead(conversation, true);
+				activity.updateConversationList();
+			}
+			this.updateSendButton();
+		}
+	}
+
+	private void decryptNext() {
+		Message next = this.mEncryptedMessages.peek();
+		PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
+
+		if (next != null && engine != null && !mDecryptJobRunning) {
+			mDecryptJobRunning = true;
+			engine.decrypt(next, new UiCallback<Message>() {
+
+				@Override
+				public void userInputRequried(PendingIntent pi, Message message) {
+					mDecryptJobRunning = false;
+					askForPassphraseIntent = pi.getIntentSender();
+					showSnackbar(R.string.openpgp_messages_found,
+							R.string.decrypt, clickToDecryptListener);
+				}
+
+				@Override
+				public void success(Message message) {
+					mDecryptJobRunning = false;
+					mEncryptedMessages.remove();
+					activity.xmppConnectionService.updateMessage(message);
+				}
+
+				@Override
+				public void error(int error, Message message) {
+					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
+					mDecryptJobRunning = false;
+					mEncryptedMessages.remove();
+					activity.xmppConnectionService.updateConversationUi();
+				}
+			});
+		}
+	}
+
+	private void messageSent() {
+		int size = this.messageList.size();
+		messagesView.setSelection(size - 1);
+		mEditMessage.setText("");
+		updateChatMsgHint();
+	}
+
+	public void updateSendButton() {
+		Conversation c = this.conversation;
+		if (activity.useSendButtonToIndicateStatus() && c != null
+				&& c.getAccount().getStatus() == Account.STATUS_ONLINE) {
+			if (c.getMode() == Conversation.MODE_SINGLE) {
+				switch (c.getContact().getMostAvailableStatus()) {
+				case Presences.CHAT:
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_online);
+					break;
+				case Presences.ONLINE:
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_online);
+					break;
+				case Presences.AWAY:
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_away);
+					break;
+				case Presences.XA:
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_away);
+					break;
+				case Presences.DND:
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_dnd);
+					break;
+				default:
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_offline);
+					break;
+				}
+			} else if (c.getMode() == Conversation.MODE_MULTI) {
+				if (c.getMucOptions().online()) {
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_online);
+				} else {
+					this.mSendButton
+							.setImageResource(R.drawable.ic_action_send_now_offline);
+				}
+			} else {
+				this.mSendButton
+						.setImageResource(R.drawable.ic_action_send_now_offline);
+			}
+		} else {
+			this.mSendButton
+					.setImageResource(R.drawable.ic_action_send_now_offline);
+		}
+	}
+
+	protected void updateStatusMessages() {
+		if (conversation.getMode() == Conversation.MODE_SINGLE) {
+			for (int i = this.messageList.size() - 1; i >= 0; --i) {
+				if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
+					return;
+				} else {
+					if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
+						this.messageList.add(i + 1,
+								Message.createStatusMessage(conversation));
+						return;
+					}
+				}
+			}
+		}
+	}
+
+	protected void makeFingerprintWarning(int latestEncryption) {
+		Set<String> knownFingerprints = conversation.getContact()
+				.getOtrFingerprints();
+		if ((latestEncryption == Message.ENCRYPTION_OTR)
+				&& (conversation.hasValidOtrSession()
+						&& (!conversation.isMuted())
+						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
+							.contains(conversation.getOtrFingerprint())))) {
+			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
+					new OnClickListener() {
+
+						@Override
+						public void onClick(View v) {
+							if (conversation.getOtrFingerprint() != null) {
+								AlertDialog dialog = UIHelper
+										.getVerifyFingerprintDialog(
+												(ConversationActivity) getActivity(),
+												conversation, snackbar);
+								dialog.show();
+							}
+						}
+					});
+		}
+	}
+
+	protected void showSnackbar(int message, int action,
+			OnClickListener clickListener) {
+		snackbar.setVisibility(View.VISIBLE);
+		snackbar.setOnClickListener(null);
+		snackbarMessage.setText(message);
+		snackbarMessage.setOnClickListener(null);
+		snackbarAction.setText(action);
+		snackbarAction.setOnClickListener(clickListener);
+	}
+
+	protected void hideSnackbar() {
+		snackbar.setVisibility(View.GONE);
+	}
+
+	protected void sendPlainTextMessage(Message message) {
+		ConversationActivity activity = (ConversationActivity) getActivity();
+		activity.xmppConnectionService.sendMessage(message);
+		messageSent();
+	}
+
+	protected void sendPgpMessage(final Message message) {
+		final ConversationActivity activity = (ConversationActivity) getActivity();
+		final XmppConnectionService xmppService = activity.xmppConnectionService;
+		final Contact contact = message.getConversation().getContact();
+		if (activity.hasPgp()) {
+			if (conversation.getMode() == Conversation.MODE_SINGLE) {
+				if (contact.getPgpKeyId() != 0) {
+					xmppService.getPgpEngine().hasKey(contact,
+							new UiCallback<Contact>() {
+
+								@Override
+								public void userInputRequried(PendingIntent pi,
+										Contact contact) {
+									activity.runIntent(
+											pi,
+											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
+								}
+
+								@Override
+								public void success(Contact contact) {
+									messageSent();
+									activity.encryptTextMessage(message);
+								}
+
+								@Override
+								public void error(int error, Contact contact) {
+
+								}
+							});
+
+				} else {
+					showNoPGPKeyDialog(false,
+							new DialogInterface.OnClickListener() {
+
+								@Override
+								public void onClick(DialogInterface dialog,
+										int which) {
+									conversation
+											.setNextEncryption(Message.ENCRYPTION_NONE);
+									xmppService.databaseBackend
+											.updateConversation(conversation);
+									message.setEncryption(Message.ENCRYPTION_NONE);
+									xmppService.sendMessage(message);
+									messageSent();
+								}
+							});
+				}
+			} else {
+				if (conversation.getMucOptions().pgpKeysInUse()) {
+					if (!conversation.getMucOptions().everybodyHasKeys()) {
+						Toast warning = Toast
+								.makeText(getActivity(),
+										R.string.missing_public_keys,
+										Toast.LENGTH_LONG);
+						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
+						warning.show();
+					}
+					activity.encryptTextMessage(message);
+					messageSent();
+				} else {
+					showNoPGPKeyDialog(true,
+							new DialogInterface.OnClickListener() {
+
+								@Override
+								public void onClick(DialogInterface dialog,
+										int which) {
+									conversation
+											.setNextEncryption(Message.ENCRYPTION_NONE);
+									message.setEncryption(Message.ENCRYPTION_NONE);
+									xmppService.databaseBackend
+											.updateConversation(conversation);
+									xmppService.sendMessage(message);
+									messageSent();
+								}
+							});
+				}
+			}
+		} else {
+			activity.showInstallPgpDialog();
+		}
+	}
+
+	public void showNoPGPKeyDialog(boolean plural,
+			DialogInterface.OnClickListener listener) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+		builder.setIconAttribute(android.R.attr.alertDialogIcon);
+		if (plural) {
+			builder.setTitle(getString(R.string.no_pgp_keys));
+			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
+		} else {
+			builder.setTitle(getString(R.string.no_pgp_key));
+			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
+		}
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		builder.setPositiveButton(getString(R.string.send_unencrypted),
+				listener);
+		builder.create().show();
+	}
+
+	protected void sendOtrMessage(final Message message) {
+		final ConversationActivity activity = (ConversationActivity) getActivity();
+		final XmppConnectionService xmppService = activity.xmppConnectionService;
+		if (conversation.hasValidOtrSession()) {
+			activity.xmppConnectionService.sendMessage(message);
+			messageSent();
+		} else {
+			activity.selectPresence(message.getConversation(),
+					new OnPresenceSelected() {
+
+						@Override
+						public void onPresenceSelected() {
+							message.setPresence(conversation.getNextPresence());
+							xmppService.sendMessage(message);
+							messageSent();
+						}
+					});
+		}
+	}
+
+	public void setText(String text) {
+		this.pastedText = text;
+	}
+
+	public void clearInputField() {
+		this.mEditMessage.setText("");
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java 🔗

@@ -0,0 +1,423 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.Validator;
+import eu.siacs.conversations.xmpp.XmppConnection.Features;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class EditAccountActivity extends XmppActivity {
+
+	private AutoCompleteTextView mAccountJid;
+	private EditText mPassword;
+	private EditText mPasswordConfirm;
+	private CheckBox mRegisterNew;
+	private Button mCancelButton;
+	private Button mSaveButton;
+
+	private LinearLayout mStats;
+	private TextView mServerInfoSm;
+	private TextView mServerInfoCarbons;
+	private TextView mServerInfoPep;
+	private TextView mSessionEst;
+	private TextView mOtrFingerprint;
+	private RelativeLayout mOtrFingerprintBox;
+	private ImageButton mOtrFingerprintToClipboardButton;
+
+	private String jidToEdit;
+	private Account mAccount;
+
+	private boolean mFetchingAvatar = false;
+
+	private OnClickListener mSaveButtonClickListener = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			if (mAccount != null
+					&& mAccount.getStatus() == Account.STATUS_DISABLED) {
+				mAccount.setOption(Account.OPTION_DISABLED, false);
+				xmppConnectionService.updateAccount(mAccount);
+				return;
+			}
+			if (!Validator.isValidJid(mAccountJid.getText().toString())) {
+				mAccountJid.setError(getString(R.string.invalid_jid));
+				mAccountJid.requestFocus();
+				return;
+			}
+			boolean registerNewAccount = mRegisterNew.isChecked();
+			String[] jidParts = mAccountJid.getText().toString().split("@");
+			String username = jidParts[0];
+			String server;
+			if (jidParts.length >= 2) {
+				server = jidParts[1];
+			} else {
+				server = "";
+			}
+			String password = mPassword.getText().toString();
+			String passwordConfirm = mPasswordConfirm.getText().toString();
+			if (registerNewAccount) {
+				if (!password.equals(passwordConfirm)) {
+					mPasswordConfirm
+							.setError(getString(R.string.passwords_do_not_match));
+					mPasswordConfirm.requestFocus();
+					return;
+				}
+			}
+			if (mAccount != null) {
+				mAccount.setPassword(password);
+				mAccount.setUsername(username);
+				mAccount.setServer(server);
+				mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+				xmppConnectionService.updateAccount(mAccount);
+			} else {
+				if (xmppConnectionService.findAccountByJid(mAccountJid
+						.getText().toString()) != null) {
+					mAccountJid
+							.setError(getString(R.string.account_already_exists));
+					mAccountJid.requestFocus();
+					return;
+				}
+				mAccount = new Account(username, server, password);
+				mAccount.setOption(Account.OPTION_USETLS, true);
+				mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
+				mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+				xmppConnectionService.createAccount(mAccount);
+			}
+			if (jidToEdit != null) {
+				finish();
+			} else {
+				updateSaveButton();
+				updateAccountInformation();
+			}
+
+		}
+	};
+	private OnClickListener mCancelButtonClickListener = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			finish();
+		}
+	};
+	private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() {
+
+		@Override
+		public void onAccountUpdate() {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					if (mAccount != null
+							&& mAccount.getStatus() != Account.STATUS_ONLINE
+							&& mFetchingAvatar) {
+						startActivity(new Intent(getApplicationContext(),
+								ManageAccountActivity.class));
+						finish();
+					} else if (jidToEdit == null && mAccount != null
+							&& mAccount.getStatus() == Account.STATUS_ONLINE) {
+						if (!mFetchingAvatar) {
+							mFetchingAvatar = true;
+							xmppConnectionService.checkForAvatar(mAccount,
+									mAvatarFetchCallback);
+						}
+					} else {
+						updateSaveButton();
+					}
+					if (mAccount != null) {
+						updateAccountInformation();
+					}
+				}
+			});
+		}
+	};
+	private UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
+
+		@Override
+		public void userInputRequried(PendingIntent pi, Avatar avatar) {
+			finishInitialSetup(avatar);
+		}
+
+		@Override
+		public void success(Avatar avatar) {
+			finishInitialSetup(avatar);
+		}
+
+		@Override
+		public void error(int errorCode, Avatar avatar) {
+			finishInitialSetup(avatar);
+		}
+	};
+	private KnownHostsAdapter mKnownHostsAdapter;
+	private TextWatcher mTextWatcher = new TextWatcher() {
+
+		@Override
+		public void onTextChanged(CharSequence s, int start, int before,
+				int count) {
+			updateSaveButton();
+		}
+
+		@Override
+		public void beforeTextChanged(CharSequence s, int start, int count,
+				int after) {
+
+		}
+
+		@Override
+		public void afterTextChanged(Editable s) {
+
+		}
+	};
+
+	protected void finishInitialSetup(final Avatar avatar) {
+		runOnUiThread(new Runnable() {
+
+			@Override
+			public void run() {
+				Intent intent;
+				if (avatar != null) {
+					intent = new Intent(getApplicationContext(),
+							StartConversationActivity.class);
+				} else {
+					intent = new Intent(getApplicationContext(),
+							PublishProfilePictureActivity.class);
+					intent.putExtra("account", mAccount.getJid());
+					intent.putExtra("setup", true);
+				}
+				startActivity(intent);
+				finish();
+			}
+		});
+	}
+
+	protected boolean inputDataDiffersFromAccount() {
+		if (mAccount == null) {
+			return true;
+		} else {
+			return (!mAccount.getJid().equals(mAccountJid.getText().toString()))
+					|| (!mAccount.getPassword().equals(
+							mPassword.getText().toString()) || mAccount
+							.isOptionSet(Account.OPTION_REGISTER) != mRegisterNew
+							.isChecked());
+		}
+	}
+
+	protected void updateSaveButton() {
+		if (mAccount != null
+				&& mAccount.getStatus() == Account.STATUS_CONNECTING) {
+			this.mSaveButton.setEnabled(false);
+			this.mSaveButton.setTextColor(getSecondaryTextColor());
+			this.mSaveButton.setText(R.string.account_status_connecting);
+		} else if (mAccount != null
+				&& mAccount.getStatus() == Account.STATUS_DISABLED) {
+			this.mSaveButton.setEnabled(true);
+			this.mSaveButton.setTextColor(getPrimaryTextColor());
+			this.mSaveButton.setText(R.string.enable);
+		} else {
+			this.mSaveButton.setEnabled(true);
+			this.mSaveButton.setTextColor(getPrimaryTextColor());
+			if (jidToEdit != null) {
+				if (mAccount != null
+						&& mAccount.getStatus() == Account.STATUS_ONLINE) {
+					this.mSaveButton.setText(R.string.save);
+					if (!accountInfoEdited()) {
+						this.mSaveButton.setEnabled(false);
+						this.mSaveButton.setTextColor(getSecondaryTextColor());
+					}
+				} else {
+					this.mSaveButton.setText(R.string.connect);
+				}
+			} else {
+				this.mSaveButton.setText(R.string.next);
+			}
+		}
+	}
+
+	protected boolean accountInfoEdited() {
+		return (!this.mAccount.getJid().equals(
+				this.mAccountJid.getText().toString()))
+				|| (!this.mAccount.getPassword().equals(
+						this.mPassword.getText().toString()));
+	}
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.activity_edit_account);
+		this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid);
+		this.mAccountJid.addTextChangedListener(this.mTextWatcher);
+		this.mPassword = (EditText) findViewById(R.id.account_password);
+		this.mPassword.addTextChangedListener(this.mTextWatcher);
+		this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm);
+		this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new);
+		this.mStats = (LinearLayout) findViewById(R.id.stats);
+		this.mSessionEst = (TextView) findViewById(R.id.session_est);
+		this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons);
+		this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
+		this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
+		this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
+		this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
+		this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
+		this.mSaveButton = (Button) findViewById(R.id.save_button);
+		this.mCancelButton = (Button) findViewById(R.id.cancel_button);
+		this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
+		this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener);
+		this.mRegisterNew
+				.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+					@Override
+					public void onCheckedChanged(CompoundButton buttonView,
+							boolean isChecked) {
+						if (isChecked) {
+							mPasswordConfirm.setVisibility(View.VISIBLE);
+						} else {
+							mPasswordConfirm.setVisibility(View.GONE);
+						}
+						updateSaveButton();
+					}
+				});
+	}
+
+	@Override
+	protected void onStart() {
+		super.onStart();
+		if (getIntent() != null) {
+			this.jidToEdit = getIntent().getStringExtra("jid");
+			if (this.jidToEdit != null) {
+				this.mRegisterNew.setVisibility(View.GONE);
+				getActionBar().setTitle(jidToEdit);
+			} else {
+				getActionBar().setTitle(R.string.action_add_account);
+			}
+		}
+	}
+
+	@Override
+	protected void onStop() {
+		if (xmppConnectionServiceBound) {
+			xmppConnectionService.removeOnAccountListChangedListener();
+		}
+		super.onStop();
+	}
+
+	@Override
+	protected void onBackendConnected() {
+		this.mKnownHostsAdapter = new KnownHostsAdapter(this,
+				android.R.layout.simple_list_item_1,
+				xmppConnectionService.getKnownHosts());
+		this.xmppConnectionService
+				.setOnAccountListChangedListener(this.mOnAccountUpdateListener);
+		if (this.jidToEdit != null) {
+			this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
+			updateAccountInformation();
+		} else if (this.xmppConnectionService.getAccounts().size() == 0) {
+			getActionBar().setDisplayHomeAsUpEnabled(false);
+			getActionBar().setDisplayShowHomeEnabled(false);
+			this.mCancelButton.setEnabled(false);
+			this.mCancelButton.setTextColor(getSecondaryTextColor());
+		}
+		this.mAccountJid.setAdapter(this.mKnownHostsAdapter);
+		updateSaveButton();
+	}
+
+	private void updateAccountInformation() {
+		this.mAccountJid.setText(this.mAccount.getJid());
+		this.mPassword.setText(this.mAccount.getPassword());
+		if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
+			this.mRegisterNew.setVisibility(View.VISIBLE);
+			this.mRegisterNew.setChecked(true);
+			this.mPasswordConfirm.setText(this.mAccount.getPassword());
+		} else {
+			this.mRegisterNew.setVisibility(View.GONE);
+			this.mRegisterNew.setChecked(false);
+		}
+		if (this.mAccount.getStatus() == Account.STATUS_ONLINE
+				&& !this.mFetchingAvatar) {
+			this.mStats.setVisibility(View.VISIBLE);
+			this.mSessionEst.setText(UIHelper.readableTimeDifference(
+					getApplicationContext(), this.mAccount.getXmppConnection()
+							.getLastSessionEstablished()));
+			Features features = this.mAccount.getXmppConnection().getFeatures();
+			if (features.carbons()) {
+				this.mServerInfoCarbons.setText(R.string.server_info_available);
+			} else {
+				this.mServerInfoCarbons
+						.setText(R.string.server_info_unavailable);
+			}
+			if (features.sm()) {
+				this.mServerInfoSm.setText(R.string.server_info_available);
+			} else {
+				this.mServerInfoSm.setText(R.string.server_info_unavailable);
+			}
+			if (features.pubsub()) {
+				this.mServerInfoPep.setText(R.string.server_info_available);
+			} else {
+				this.mServerInfoPep.setText(R.string.server_info_unavailable);
+			}
+			final String fingerprint = this.mAccount
+					.getOtrFingerprint(xmppConnectionService);
+			if (fingerprint != null) {
+				this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
+				this.mOtrFingerprint.setText(fingerprint);
+				this.mOtrFingerprintToClipboardButton
+						.setVisibility(View.VISIBLE);
+				this.mOtrFingerprintToClipboardButton
+						.setOnClickListener(new View.OnClickListener() {
+
+							@Override
+							public void onClick(View v) {
+
+								if (OtrFingerprintToClipBoard(fingerprint)) {
+									Toast.makeText(
+											EditAccountActivity.this,
+											R.string.toast_message_otr_fingerprint,
+											Toast.LENGTH_SHORT).show();
+								}
+							}
+						});
+			} else {
+				this.mOtrFingerprintBox.setVisibility(View.GONE);
+			}
+		} else {
+			if (this.mAccount.errorStatus()) {
+				this.mAccountJid.setError(getString(this.mAccount
+						.getReadableStatusId()));
+				this.mAccountJid.requestFocus();
+			}
+			this.mStats.setVisibility(View.GONE);
+		}
+	}
+
+	private boolean OtrFingerprintToClipBoard(String fingerprint) {
+		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+		String label = getResources().getString(R.string.otr_fingerprint);
+		if (mClipBoardManager != null) {
+			ClipData mClipData = ClipData.newPlainText(label, fingerprint);
+			mClipBoardManager.setPrimaryClip(mClipData);
+			return true;
+		}
+		return false;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java 🔗

@@ -0,0 +1,39 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+public class EditMessage extends EditText {
+
+	public EditMessage(Context context, AttributeSet attrs) {
+		super(context, attrs);
+	}
+
+	public EditMessage(Context context) {
+		super(context);
+	}
+
+	protected OnEnterPressed mOnEnterPressed;
+
+	@Override
+	public boolean onKeyDown(int keyCode, KeyEvent event) {
+		if (keyCode == KeyEvent.KEYCODE_ENTER) {
+			if (mOnEnterPressed != null) {
+				mOnEnterPressed.onEnterPressed();
+			}
+			return true;
+		}
+		return super.onKeyDown(keyCode, event);
+	}
+
+	public void setOnEnterPressedListener(OnEnterPressed listener) {
+		this.mOnEnterPressed = listener;
+	}
+
+	public interface OnEnterPressed {
+		public void onEnterPressed();
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java 🔗

@@ -0,0 +1,217 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+public class ManageAccountActivity extends XmppActivity {
+
+	protected Account selectedAccount = null;
+
+	protected List<Account> accountList = new ArrayList<Account>();
+	protected ListView accountListView;
+	protected AccountAdapter mAccountAdapter;
+	protected OnAccountUpdate accountChanged = new OnAccountUpdate() {
+
+		@Override
+		public void onAccountUpdate() {
+			accountList.clear();
+			accountList.addAll(xmppConnectionService.getAccounts());
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					mAccountAdapter.notifyDataSetChanged();
+				}
+			});
+		}
+	};
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+
+		super.onCreate(savedInstanceState);
+
+		setContentView(R.layout.manage_accounts);
+
+		accountListView = (ListView) findViewById(R.id.account_list);
+		this.mAccountAdapter = new AccountAdapter(this, accountList);
+		accountListView.setAdapter(this.mAccountAdapter);
+		accountListView.setOnItemClickListener(new OnItemClickListener() {
+
+			@Override
+			public void onItemClick(AdapterView<?> arg0, View view,
+					int position, long arg3) {
+				switchToAccount(accountList.get(position));
+			}
+		});
+		registerForContextMenu(accountListView);
+	}
+
+	@Override
+	public void onCreateContextMenu(ContextMenu menu, View v,
+			ContextMenuInfo menuInfo) {
+		super.onCreateContextMenu(menu, v, menuInfo);
+		ManageAccountActivity.this.getMenuInflater().inflate(
+				R.menu.manageaccounts_context, menu);
+		AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+		this.selectedAccount = accountList.get(acmi.position);
+		if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) {
+			menu.findItem(R.id.mgmt_account_disable).setVisible(false);
+			menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
+			menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
+		} else {
+			menu.findItem(R.id.mgmt_account_enable).setVisible(false);
+		}
+		menu.setHeaderTitle(this.selectedAccount.getJid());
+	}
+
+	@Override
+	protected void onStop() {
+		if (xmppConnectionServiceBound) {
+			xmppConnectionService.removeOnAccountListChangedListener();
+		}
+		super.onStop();
+	}
+
+	@Override
+	void onBackendConnected() {
+		xmppConnectionService.setOnAccountListChangedListener(accountChanged);
+		this.accountList.clear();
+		this.accountList.addAll(xmppConnectionService.getAccounts());
+		mAccountAdapter.notifyDataSetChanged();
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.manageaccounts, menu);
+		return true;
+	}
+
+	@Override
+	public boolean onContextItemSelected(MenuItem item) {
+		switch (item.getItemId()) {
+		case R.id.mgmt_account_publish_avatar:
+			publishAvatar(selectedAccount);
+			return true;
+		case R.id.mgmt_account_disable:
+			disableAccount(selectedAccount);
+			return true;
+		case R.id.mgmt_account_enable:
+			enableAccount(selectedAccount);
+			return true;
+		case R.id.mgmt_account_delete:
+			deleteAccount(selectedAccount);
+			return true;
+		case R.id.mgmt_account_announce_pgp:
+			publishOpenPGPPublicKey(selectedAccount);
+		default:
+			return super.onContextItemSelected(item);
+		}
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(MenuItem item) {
+		switch (item.getItemId()) {
+		case R.id.action_add_account:
+			startActivity(new Intent(getApplicationContext(),
+					EditAccountActivity.class));
+			break;
+		default:
+			break;
+		}
+		return super.onOptionsItemSelected(item);
+	}
+
+	@Override
+	public boolean onNavigateUp() {
+		if (xmppConnectionService.getConversations().size() == 0) {
+			Intent contactsIntent = new Intent(this,
+					StartConversationActivity.class);
+			contactsIntent.setFlags(
+			// if activity exists in stack, pop the stack and go back to it
+					Intent.FLAG_ACTIVITY_CLEAR_TOP |
+					// otherwise, make a new task for it
+							Intent.FLAG_ACTIVITY_NEW_TASK |
+							// don't use the new activity animation; finish
+							// animation runs instead
+							Intent.FLAG_ACTIVITY_NO_ANIMATION);
+			startActivity(contactsIntent);
+			finish();
+			return true;
+		} else {
+			return super.onNavigateUp();
+		}
+	}
+
+	private void publishAvatar(Account account) {
+		Intent intent = new Intent(getApplicationContext(),
+				PublishProfilePictureActivity.class);
+		intent.putExtra("account", account.getJid());
+		startActivity(intent);
+	}
+
+	private void disableAccount(Account account) {
+		account.setOption(Account.OPTION_DISABLED, true);
+		xmppConnectionService.updateAccount(account);
+	}
+
+	private void enableAccount(Account account) {
+		account.setOption(Account.OPTION_DISABLED, false);
+		xmppConnectionService.updateAccount(account);
+	}
+
+	private void publishOpenPGPPublicKey(Account account) {
+		if (ManageAccountActivity.this.hasPgp()) {
+			announcePgp(account, null);
+		} else {
+			this.showInstallPgpDialog();
+		}
+	}
+
+	private void deleteAccount(final Account account) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(
+				ManageAccountActivity.this);
+		builder.setTitle(getString(R.string.mgmt_account_are_you_sure));
+		builder.setIconAttribute(android.R.attr.alertDialogIcon);
+		builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text));
+		builder.setPositiveButton(getString(R.string.delete),
+				new OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						xmppConnectionService.deleteAccount(account);
+						selectedAccount = null;
+					}
+				});
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		builder.create().show();
+	}
+
+	@Override
+	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+		super.onActivityResult(requestCode, resultCode, data);
+		if (resultCode == RESULT_OK) {
+			if (requestCode == REQUEST_ANNOUNCE_PGP) {
+				announcePgp(selectedAccount, null);
+			}
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java 🔗

@@ -0,0 +1,242 @@
+package eu.siacs.conversations.ui;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+public class PublishProfilePictureActivity extends XmppActivity {
+
+	private static final int REQUEST_CHOOSE_FILE = 0xac23;
+
+	private ImageView avatar;
+	private TextView accountTextView;
+	private TextView hintOrWarning;
+	private TextView secondaryHint;
+	private Button cancelButton;
+	private Button publishButton;
+
+	private Uri avatarUri;
+	private Uri defaultUri;
+
+	private Account account;
+
+	private boolean support = false;
+
+	private boolean mInitialAccountSetup;
+
+	private UiCallback<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());
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java 🔗

@@ -0,0 +1,74 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+import eu.siacs.conversations.entities.Account;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.PreferenceManager;
+
+public class SettingsActivity extends XmppActivity implements
+		OnSharedPreferenceChangeListener {
+	private SettingsFragment mSettingsFragment;
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		mSettingsFragment = new SettingsFragment();
+		getFragmentManager().beginTransaction()
+				.replace(android.R.id.content, mSettingsFragment).commit();
+	}
+
+	@Override
+	void onBackendConnected() {
+
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		PreferenceManager.getDefaultSharedPreferences(this)
+				.registerOnSharedPreferenceChangeListener(this);
+		ListPreference resources = (ListPreference) mSettingsFragment
+				.findPreference("resource");
+		if (resources != null) {
+			ArrayList<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);
+					}
+				}
+			}
+		}
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java 🔗

@@ -0,0 +1,15 @@
+package eu.siacs.conversations.ui;
+
+import eu.siacs.conversations.R;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+public class SettingsFragment extends PreferenceFragment {
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		// Load the preferences from an XML resource
+		addPreferencesFromResource(R.xml.preferences);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java 🔗

@@ -0,0 +1,185 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.adapter.ConversationAdapter;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.Toast;
+
+public class ShareWithActivity extends XmppActivity {
+
+	private class Share {
+		public Uri uri;
+		public String account;
+		public String contact;
+		public String text;
+	}
+
+	private Share share;
+
+	private static final int REQUEST_START_NEW_CONVERSATION = 0x0501;
+	private ListView mListView;
+	private List<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();
+		}
+
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -0,0 +1,677 @@
+package eu.siacs.conversations.ui;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.ActionBar.TabListener;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
+import eu.siacs.conversations.ui.adapter.ListItemAdapter;
+import eu.siacs.conversations.utils.Validator;
+
+public class StartConversationActivity extends XmppActivity {
+
+	private Tab mContactsTab;
+	private Tab mConferencesTab;
+	private ViewPager mViewPager;
+
+	private MyListFragment mContactsListFragment = new MyListFragment();
+	private List<ListItem> contacts = new ArrayList<ListItem>();
+	private ArrayAdapter<ListItem> mContactsAdapter;
+
+	private MyListFragment mConferenceListFragment = new MyListFragment();
+	private List<ListItem> conferences = new ArrayList<ListItem>();
+	private ArrayAdapter<ListItem> mConferenceAdapter;
+
+	private List<String> mActivatedAccounts = new ArrayList<String>();
+	private List<String> mKnownHosts;
+	private List<String> mKnownConferenceHosts;
+
+	private Menu mOptionsMenu;
+	private EditText mSearchEditText;
+
+	public int conference_context_id;
+	public int contact_context_id;
+
+	private TabListener mTabListener = new TabListener() {
+
+		@Override
+		public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+			return;
+		}
+
+		@Override
+		public void onTabSelected(Tab tab, FragmentTransaction ft) {
+			mViewPager.setCurrentItem(tab.getPosition());
+			onTabChanged();
+		}
+
+		@Override
+		public void onTabReselected(Tab tab, FragmentTransaction ft) {
+			return;
+		}
+	};
+
+	private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
+		@Override
+		public void onPageSelected(int position) {
+			getActionBar().setSelectedNavigationItem(position);
+			onTabChanged();
+		}
+	};
+
+	private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
+
+		@Override
+		public boolean onMenuItemActionExpand(MenuItem item) {
+			mSearchEditText.post(new Runnable() {
+
+				@Override
+				public void run() {
+					mSearchEditText.requestFocus();
+					InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+					imm.showSoftInput(mSearchEditText,
+							InputMethodManager.SHOW_IMPLICIT);
+				}
+			});
+
+			return true;
+		}
+
+		@Override
+		public boolean onMenuItemActionCollapse(MenuItem item) {
+			InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+			imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(),
+					InputMethodManager.HIDE_IMPLICIT_ONLY);
+			mSearchEditText.setText("");
+			filter(null);
+			return true;
+		}
+	};
+	private TextWatcher mSearchTextWatcher = new TextWatcher() {
+
+		@Override
+		public void afterTextChanged(Editable editable) {
+			filter(editable.toString());
+		}
+
+		@Override
+		public void beforeTextChanged(CharSequence s, int start, int count,
+				int after) {
+		}
+
+		@Override
+		public void onTextChanged(CharSequence s, int start, int before,
+				int count) {
+		}
+	};
+	private OnRosterUpdate onRosterUpdate = new OnRosterUpdate() {
+
+		@Override
+		public void onRosterUpdate() {
+			runOnUiThread(new Runnable() {
+
+				@Override
+				public void run() {
+					if (mSearchEditText != null) {
+						filter(mSearchEditText.getText().toString());
+					}
+				}
+			});
+		}
+	};
+	private MenuItem mMenuSearchView;
+	private String mInitialJid;
+
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.activity_start_conversation);
+		mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager);
+		ActionBar actionBar = getActionBar();
+		actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+		mContactsTab = actionBar.newTab().setText(R.string.contacts)
+				.setTabListener(mTabListener);
+		mConferencesTab = actionBar.newTab().setText(R.string.conferences)
+				.setTabListener(mTabListener);
+		actionBar.addTab(mContactsTab);
+		actionBar.addTab(mConferencesTab);
+
+		mViewPager.setOnPageChangeListener(mOnPageChangeListener);
+		mViewPager.setAdapter(new FragmentPagerAdapter(getFragmentManager()) {
+
+			@Override
+			public int getCount() {
+				return 2;
+			}
+
+			@Override
+			public Fragment getItem(int position) {
+				if (position == 0) {
+					return mContactsListFragment;
+				} else {
+					return mConferenceListFragment;
+				}
+			}
+		});
+
+		mConferenceAdapter = new ListItemAdapter(this, conferences);
+		mConferenceListFragment.setListAdapter(mConferenceAdapter);
+		mConferenceListFragment.setContextMenu(R.menu.conference_context);
+		mConferenceListFragment
+				.setOnListItemClickListener(new OnItemClickListener() {
+
+					@Override
+					public void onItemClick(AdapterView<?> arg0, View arg1,
+							int position, long arg3) {
+						openConversationForBookmark(position);
+					}
+				});
+
+		mContactsAdapter = new ListItemAdapter(this, contacts);
+		mContactsListFragment.setListAdapter(mContactsAdapter);
+		mContactsListFragment.setContextMenu(R.menu.contact_context);
+		mContactsListFragment
+				.setOnListItemClickListener(new OnItemClickListener() {
+
+					@Override
+					public void onItemClick(AdapterView<?> arg0, View arg1,
+							int position, long arg3) {
+						openConversationForContact(position);
+					}
+				});
+
+	}
+
+	@Override
+	public void onStop() {
+		super.onStop();
+		xmppConnectionService.removeOnRosterUpdateListener();
+	}
+
+	protected void openConversationForContact(int position) {
+		Contact contact = (Contact) contacts.get(position);
+		Conversation conversation = xmppConnectionService
+				.findOrCreateConversation(contact.getAccount(),
+						contact.getJid(), false);
+		switchToConversation(conversation);
+	}
+
+	protected void openConversationForContact() {
+		int position = contact_context_id;
+		openConversationForContact(position);
+	}
+
+	protected void openConversationForBookmark() {
+		openConversationForBookmark(conference_context_id);
+	}
+
+	protected void openConversationForBookmark(int position) {
+		Bookmark bookmark = (Bookmark) conferences.get(position);
+		Conversation conversation = xmppConnectionService
+				.findOrCreateConversation(bookmark.getAccount(),
+						bookmark.getJid(), true);
+		conversation.setBookmark(bookmark);
+		if (!conversation.getMucOptions().online()) {
+			xmppConnectionService.joinMuc(conversation);
+		}
+		if (!bookmark.autojoin()) {
+			bookmark.setAutojoin(true);
+			xmppConnectionService.pushBookmarks(bookmark.getAccount());
+		}
+		switchToConversation(conversation);
+	}
+
+	protected void openDetailsForContact() {
+		int position = contact_context_id;
+		Contact contact = (Contact) contacts.get(position);
+		switchToContactDetails(contact);
+	}
+
+	protected void deleteContact() {
+		int position = contact_context_id;
+		final Contact contact = (Contact) contacts.get(position);
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setTitle(R.string.action_delete_contact);
+		builder.setMessage(getString(R.string.remove_contact_text,
+				contact.getJid()));
+		builder.setPositiveButton(R.string.delete, new OnClickListener() {
+
+			@Override
+			public void onClick(DialogInterface dialog, int which) {
+				xmppConnectionService.deleteContactOnServer(contact);
+				filter(mSearchEditText.getText().toString());
+			}
+		});
+		builder.create().show();
+
+	}
+
+	protected void deleteConference() {
+		int position = conference_context_id;
+		final Bookmark bookmark = (Bookmark) conferences.get(position);
+
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setTitle(R.string.delete_bookmark);
+		builder.setMessage(getString(R.string.remove_bookmark_text,
+				bookmark.getJid()));
+		builder.setPositiveButton(R.string.delete, new OnClickListener() {
+
+			@Override
+			public void onClick(DialogInterface dialog, int which) {
+				bookmark.unregisterConversation();
+				Account account = bookmark.getAccount();
+				account.getBookmarks().remove(bookmark);
+				xmppConnectionService.pushBookmarks(account);
+				filter(mSearchEditText.getText().toString());
+			}
+		});
+		builder.create().show();
+
+	}
+
+	@SuppressLint("InflateParams")
+	protected void showCreateContactDialog(String prefilledJid) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(R.string.create_contact);
+		View dialogView = getLayoutInflater().inflate(
+				R.layout.create_contact_dialog, null);
+		final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
+		final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView
+				.findViewById(R.id.jid);
+		jid.setAdapter(new KnownHostsAdapter(this,
+				android.R.layout.simple_list_item_1, mKnownHosts));
+		if (prefilledJid != null) {
+			jid.append(prefilledJid);
+		}
+		populateAccountSpinner(spinner);
+		builder.setView(dialogView);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setPositiveButton(R.string.create, null);
+		final AlertDialog dialog = builder.create();
+		dialog.show();
+		dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
+				new View.OnClickListener() {
+
+					@Override
+					public void onClick(View v) {
+						if (!xmppConnectionServiceBound) {
+							return;
+						}
+						if (Validator.isValidJid(jid.getText().toString())) {
+							String accountJid = (String) spinner
+									.getSelectedItem();
+							String contactJid = jid.getText().toString();
+							Account account = xmppConnectionService
+									.findAccountByJid(accountJid);
+							if (account == null) {
+								dialog.dismiss();
+								return;
+							}
+							Contact contact = account.getRoster().getContact(
+									contactJid);
+							if (contact.showInRoster()) {
+								jid.setError(getString(R.string.contact_already_exists));
+							} else {
+								xmppConnectionService.createContact(contact);
+								dialog.dismiss();
+								switchToConversation(contact);
+							}
+						} else {
+							jid.setError(getString(R.string.invalid_jid));
+						}
+					}
+				});
+
+	}
+
+	@SuppressLint("InflateParams")
+	protected void showJoinConferenceDialog() {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(R.string.join_conference);
+		View dialogView = getLayoutInflater().inflate(
+				R.layout.join_conference_dialog, null);
+		final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
+		final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView
+				.findViewById(R.id.jid);
+		jid.setAdapter(new KnownHostsAdapter(this,
+				android.R.layout.simple_list_item_1, mKnownConferenceHosts));
+		populateAccountSpinner(spinner);
+		final CheckBox bookmarkCheckBox = (CheckBox) dialogView
+				.findViewById(R.id.bookmark);
+		builder.setView(dialogView);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setPositiveButton(R.string.join, null);
+		final AlertDialog dialog = builder.create();
+		dialog.show();
+		dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
+				new View.OnClickListener() {
+
+					@Override
+					public void onClick(View v) {
+						if (!xmppConnectionServiceBound) {
+							return;
+						}
+						if (Validator.isValidJid(jid.getText().toString())) {
+							String accountJid = (String) spinner
+									.getSelectedItem();
+							String conferenceJid = jid.getText().toString();
+							Account account = xmppConnectionService
+									.findAccountByJid(accountJid);
+							if (account == null) {
+								dialog.dismiss();
+								return;
+							}
+							if (bookmarkCheckBox.isChecked()) {
+								if (account.hasBookmarkFor(conferenceJid)) {
+									jid.setError(getString(R.string.bookmark_already_exists));
+								} else {
+									Bookmark bookmark = new Bookmark(account,
+											conferenceJid);
+									bookmark.setAutojoin(true);
+									account.getBookmarks().add(bookmark);
+									xmppConnectionService
+											.pushBookmarks(account);
+									Conversation conversation = xmppConnectionService
+											.findOrCreateConversation(account,
+													conferenceJid, true);
+									conversation.setBookmark(bookmark);
+									if (!conversation.getMucOptions().online()) {
+										xmppConnectionService
+												.joinMuc(conversation);
+									}
+									dialog.dismiss();
+									switchToConversation(conversation);
+								}
+							} else {
+								Conversation conversation = xmppConnectionService
+										.findOrCreateConversation(account,
+												conferenceJid, true);
+								if (!conversation.getMucOptions().online()) {
+									xmppConnectionService.joinMuc(conversation);
+								}
+								dialog.dismiss();
+								switchToConversation(conversation);
+							}
+						} else {
+							jid.setError(getString(R.string.invalid_jid));
+						}
+					}
+				});
+	}
+
+	protected void switchToConversation(Contact contact) {
+		Conversation conversation = xmppConnectionService
+				.findOrCreateConversation(contact.getAccount(),
+						contact.getJid(), false);
+		switchToConversation(conversation);
+	}
+
+	private void populateAccountSpinner(Spinner spinner) {
+		ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+				android.R.layout.simple_spinner_item, mActivatedAccounts);
+		adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+		spinner.setAdapter(adapter);
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		this.mOptionsMenu = menu;
+		getMenuInflater().inflate(R.menu.start_conversation, menu);
+		MenuItem menuCreateContact = (MenuItem) menu
+				.findItem(R.id.action_create_contact);
+		MenuItem menuCreateConference = (MenuItem) menu
+				.findItem(R.id.action_join_conference);
+		mMenuSearchView = (MenuItem) menu.findItem(R.id.action_search);
+		mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
+		View mSearchView = mMenuSearchView.getActionView();
+		mSearchEditText = (EditText) mSearchView
+				.findViewById(R.id.search_field);
+		mSearchEditText.addTextChangedListener(mSearchTextWatcher);
+		if (getActionBar().getSelectedNavigationIndex() == 0) {
+			menuCreateConference.setVisible(false);
+		} else {
+			menuCreateContact.setVisible(false);
+		}
+		if (mInitialJid != null) {
+			mMenuSearchView.expandActionView();
+			mSearchEditText.append(mInitialJid);
+			filter(mInitialJid);
+		}
+		return true;
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(MenuItem item) {
+		switch (item.getItemId()) {
+		case R.id.action_create_contact:
+			showCreateContactDialog(null);
+			break;
+		case R.id.action_join_conference:
+			showJoinConferenceDialog();
+			break;
+		}
+		return super.onOptionsItemSelected(item);
+	}
+
+	@Override
+	public boolean onKeyUp(int keyCode, KeyEvent event) {
+		if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
+			mOptionsMenu.findItem(R.id.action_search).expandActionView();
+			return true;
+		}
+		return super.onKeyUp(keyCode, event);
+	}
+
+	@Override
+	protected void onBackendConnected() {
+		xmppConnectionService.setOnRosterUpdateListener(this.onRosterUpdate);
+		this.mActivatedAccounts.clear();
+		for (Account account : xmppConnectionService.getAccounts()) {
+			if (account.getStatus() != Account.STATUS_DISABLED) {
+				this.mActivatedAccounts.add(account.getJid());
+			}
+		}
+		this.mKnownHosts = xmppConnectionService.getKnownHosts();
+		this.mKnownConferenceHosts = xmppConnectionService
+				.getKnownConferenceHosts();
+		if (!startByIntent()) {
+			if (mSearchEditText != null) {
+				filter(mSearchEditText.getText().toString());
+			} else {
+				filter(null);
+			}
+		}
+	}
+
+	protected boolean startByIntent() {
+		if (getIntent() != null
+				&& Intent.ACTION_SENDTO.equals(getIntent().getAction())) {
+			try {
+				String jid = URLDecoder.decode(
+						getIntent().getData().getEncodedPath(), "UTF-8").split(
+						"/")[1];
+				setIntent(null);
+				return handleJid(jid);
+			} catch (UnsupportedEncodingException e) {
+				setIntent(null);
+				return false;
+			}
+		} else if (getIntent() != null
+				&& Intent.ACTION_VIEW.equals(getIntent().getAction())) {
+			Uri uri = getIntent().getData();
+			String jid = uri.getSchemeSpecificPart().split("\\?")[0];
+			return handleJid(jid);
+		}
+		return false;
+	}
+
+	private boolean handleJid(String jid) {
+		List<Contact> contacts = xmppConnectionService.findContacts(jid);
+		if (contacts.size() == 0) {
+			showCreateContactDialog(jid);
+			return false;
+		} else if (contacts.size() == 1) {
+			switchToConversation(contacts.get(0));
+			return true;
+		} else {
+			if (mMenuSearchView != null) {
+				mMenuSearchView.expandActionView();
+				mSearchEditText.setText(jid);
+				filter(jid);
+			} else {
+				mInitialJid = jid;
+			}
+			return true;
+		}
+	}
+
+	protected void filter(String needle) {
+		if (xmppConnectionServiceBound) {
+			this.filterContacts(needle);
+			this.filterConferences(needle);
+		}
+	}
+
+	protected void filterContacts(String needle) {
+		this.contacts.clear();
+		for (Account account : xmppConnectionService.getAccounts()) {
+			if (account.getStatus() != Account.STATUS_DISABLED) {
+				for (Contact contact : account.getRoster().getContacts()) {
+					if (contact.showInRoster() && contact.match(needle)) {
+						this.contacts.add(contact);
+					}
+				}
+			}
+		}
+		Collections.sort(this.contacts);
+		mContactsAdapter.notifyDataSetChanged();
+	}
+
+	protected void filterConferences(String needle) {
+		this.conferences.clear();
+		for (Account account : xmppConnectionService.getAccounts()) {
+			if (account.getStatus() != Account.STATUS_DISABLED) {
+				for (Bookmark bookmark : account.getBookmarks()) {
+					if (bookmark.match(needle)) {
+						this.conferences.add(bookmark);
+					}
+				}
+			}
+		}
+		Collections.sort(this.conferences);
+		mConferenceAdapter.notifyDataSetChanged();
+	}
+
+	private void onTabChanged() {
+		invalidateOptionsMenu();
+	}
+
+	public static class MyListFragment extends ListFragment {
+		private AdapterView.OnItemClickListener mOnItemClickListener;
+		private int mResContextMenu;
+
+		public void setContextMenu(int res) {
+			this.mResContextMenu = res;
+		}
+
+		@Override
+		public void onListItemClick(ListView l, View v, int position, long id) {
+			if (mOnItemClickListener != null) {
+				mOnItemClickListener.onItemClick(l, v, position, id);
+			}
+		}
+
+		public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
+			this.mOnItemClickListener = l;
+		}
+
+		@Override
+		public void onViewCreated(View view, Bundle savedInstanceState) {
+			super.onViewCreated(view, savedInstanceState);
+			registerForContextMenu(getListView());
+			getListView().setFastScrollEnabled(true);
+		}
+
+		@Override
+		public void onCreateContextMenu(ContextMenu menu, View v,
+				ContextMenuInfo menuInfo) {
+			super.onCreateContextMenu(menu, v, menuInfo);
+			StartConversationActivity activity = (StartConversationActivity) getActivity();
+			activity.getMenuInflater().inflate(mResContextMenu, menu);
+			AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+			if (mResContextMenu == R.menu.conference_context) {
+				activity.conference_context_id = acmi.position;
+			} else {
+				activity.contact_context_id = acmi.position;
+			}
+		}
+
+		@Override
+		public boolean onContextItemSelected(MenuItem item) {
+			StartConversationActivity activity = (StartConversationActivity) getActivity();
+			switch (item.getItemId()) {
+			case R.id.context_start_conversation:
+				activity.openConversationForContact();
+				break;
+			case R.id.context_contact_details:
+				activity.openDetailsForContact();
+				break;
+			case R.id.context_delete_contact:
+				activity.deleteContact();
+				break;
+			case R.id.context_join_conference:
+				activity.openConversationForBookmark();
+				break;
+			case R.id.context_delete_conference:
+				activity.deleteConference();
+			}
+			return true;
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -0,0 +1,637 @@
+package eu.siacs.conversations.ui;
+
+import java.io.FileNotFoundException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
+import eu.siacs.conversations.utils.ExceptionHelper;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.app.AlertDialog.Builder;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnClickListener;
+import android.content.IntentSender.SendIntentException;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.text.InputType;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+public abstract class XmppActivity extends Activity {
+
+	protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
+	protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
+
+	public XmppConnectionService xmppConnectionService;
+	public boolean xmppConnectionServiceBound = false;
+	protected boolean handledViewIntent = false;
+
+	protected int mPrimaryTextColor;
+	protected int mSecondaryTextColor;
+	protected int mSecondaryBackgroundColor;
+	protected int mColorRed;
+	protected int mColorOrange;
+	protected int mColorGreen;
+	protected int mPrimaryColor;
+
+	protected boolean mUseSubject = true;
+
+	private DisplayMetrics metrics;
+
+	protected interface OnValueEdited {
+		public void onValueEdited(String value);
+	}
+
+	public interface OnPresenceSelected {
+		public void onPresenceSelected();
+	}
+
+	protected ServiceConnection mConnection = new ServiceConnection() {
+
+		@Override
+		public void onServiceConnected(ComponentName className, IBinder service) {
+			XmppConnectionBinder binder = (XmppConnectionBinder) service;
+			xmppConnectionService = binder.getService();
+			xmppConnectionServiceBound = true;
+			onBackendConnected();
+		}
+
+		@Override
+		public void onServiceDisconnected(ComponentName arg0) {
+			xmppConnectionServiceBound = false;
+		}
+	};
+
+	@Override
+	protected void onStart() {
+		super.onStart();
+		if (!xmppConnectionServiceBound) {
+			connectToBackend();
+		}
+	}
+
+	public void connectToBackend() {
+		Intent intent = new Intent(this, XmppConnectionService.class);
+		intent.setAction("ui");
+		startService(intent);
+		bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+	}
+
+	@Override
+	protected void onStop() {
+		super.onStop();
+		if (xmppConnectionServiceBound) {
+			unbindService(mConnection);
+			xmppConnectionServiceBound = false;
+		}
+	}
+
+	protected void hideKeyboard() {
+		InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+		View focus = getCurrentFocus();
+
+		if (focus != null) {
+
+			inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
+					InputMethodManager.HIDE_NOT_ALWAYS);
+		}
+	}
+
+	public boolean hasPgp() {
+		return xmppConnectionService.getPgpEngine() != null;
+	}
+
+	public void showInstallPgpDialog() {
+		Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(getString(R.string.openkeychain_required));
+		builder.setIconAttribute(android.R.attr.alertDialogIcon);
+		builder.setMessage(getText(R.string.openkeychain_required_long));
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		builder.setNeutralButton(getString(R.string.restart),
+				new OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						if (xmppConnectionServiceBound) {
+							unbindService(mConnection);
+							xmppConnectionServiceBound = false;
+						}
+						stopService(new Intent(XmppActivity.this,
+								XmppConnectionService.class));
+						finish();
+					}
+				});
+		builder.setPositiveButton(getString(R.string.install),
+				new OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						Uri uri = Uri
+								.parse("market://details?id=org.sufficientlysecure.keychain");
+						Intent marketIntent = new Intent(Intent.ACTION_VIEW,
+								uri);
+						PackageManager manager = getApplicationContext()
+								.getPackageManager();
+						List<ResolveInfo> infos = manager
+								.queryIntentActivities(marketIntent, 0);
+						if (infos.size() > 0) {
+							startActivity(marketIntent);
+						} else {
+							uri = Uri.parse("http://www.openkeychain.org/");
+							Intent browserIntent = new Intent(
+									Intent.ACTION_VIEW, uri);
+							startActivity(browserIntent);
+						}
+						finish();
+					}
+				});
+		builder.create().show();
+	}
+
+	abstract void onBackendConnected();
+
+	public boolean onOptionsItemSelected(MenuItem item) {
+		switch (item.getItemId()) {
+		case R.id.action_settings:
+			startActivity(new Intent(this, SettingsActivity.class));
+			break;
+		case R.id.action_accounts:
+			startActivity(new Intent(this, ManageAccountActivity.class));
+			break;
+		case android.R.id.home:
+			finish();
+			break;
+		}
+		return super.onOptionsItemSelected(item);
+	}
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		metrics = getResources().getDisplayMetrics();
+		ExceptionHelper.init(getApplicationContext());
+		mPrimaryTextColor = getResources().getColor(R.color.primarytext);
+		mSecondaryTextColor = getResources().getColor(R.color.secondarytext);
+		mColorRed = getResources().getColor(R.color.red);
+		mColorOrange = getResources().getColor(R.color.orange);
+		mColorGreen = getResources().getColor(R.color.green);
+		mPrimaryColor = getResources().getColor(R.color.primary);
+		mSecondaryBackgroundColor = getResources().getColor(
+				R.color.secondarybackground);
+		if (getPreferences().getBoolean("use_larger_font", false)) {
+			setTheme(R.style.ConversationsTheme_LargerText);
+		}
+		mUseSubject = getPreferences().getBoolean("use_subject", true);
+	}
+
+	protected SharedPreferences getPreferences() {
+		return PreferenceManager
+				.getDefaultSharedPreferences(getApplicationContext());
+	}
+
+	public boolean useSubjectToIdentifyConference() {
+		return mUseSubject;
+	}
+
+	public void switchToConversation(Conversation conversation) {
+		switchToConversation(conversation, null, false);
+	}
+
+	public void switchToConversation(Conversation conversation, String text,
+			boolean newTask) {
+		Intent viewConversationIntent = new Intent(this,
+				ConversationActivity.class);
+		viewConversationIntent.setAction(Intent.ACTION_VIEW);
+		viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+				conversation.getUuid());
+		if (text != null) {
+			viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
+		}
+		viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+		if (newTask) {
+			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
+					| Intent.FLAG_ACTIVITY_NEW_TASK
+					| Intent.FLAG_ACTIVITY_SINGLE_TOP);
+		} else {
+			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
+					| Intent.FLAG_ACTIVITY_CLEAR_TOP);
+		}
+		startActivity(viewConversationIntent);
+		finish();
+	}
+
+	public void switchToContactDetails(Contact contact) {
+		Intent intent = new Intent(this, ContactDetailsActivity.class);
+		intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
+		intent.putExtra("account", contact.getAccount().getJid());
+		intent.putExtra("contact", contact.getJid());
+		startActivity(intent);
+	}
+
+	public void switchToAccount(Account account) {
+		Intent intent = new Intent(this, EditAccountActivity.class);
+		intent.putExtra("jid", account.getJid());
+		startActivity(intent);
+	}
+
+	protected void inviteToConversation(Conversation conversation) {
+		Intent intent = new Intent(getApplicationContext(),
+				ChooseContactActivity.class);
+		intent.putExtra("conversation", conversation.getUuid());
+		startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
+	}
+
+	protected void announcePgp(Account account, final Conversation conversation) {
+		xmppConnectionService.getPgpEngine().generateSignature(account,
+				"online", new UiCallback<Account>() {
+
+					@Override
+					public void userInputRequried(PendingIntent pi,
+							Account account) {
+						try {
+							startIntentSenderForResult(pi.getIntentSender(),
+									REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+						} catch (SendIntentException e) {
+						}
+					}
+
+					@Override
+					public void success(Account account) {
+						xmppConnectionService.databaseBackend
+								.updateAccount(account);
+						xmppConnectionService.sendPresencePacket(account,
+								xmppConnectionService.getPresenceGenerator()
+										.sendPresence(account));
+						if (conversation != null) {
+							conversation
+									.setNextEncryption(Message.ENCRYPTION_PGP);
+							xmppConnectionService.databaseBackend
+									.updateConversation(conversation);
+						}
+					}
+
+					@Override
+					public void error(int error, Account account) {
+						displayErrorDialog(error);
+					}
+				});
+	}
+
+	protected void displayErrorDialog(final int errorCode) {
+		runOnUiThread(new Runnable() {
+
+			@Override
+			public void run() {
+				AlertDialog.Builder builder = new AlertDialog.Builder(
+						XmppActivity.this);
+				builder.setIconAttribute(android.R.attr.alertDialogIcon);
+				builder.setTitle(getString(R.string.error));
+				builder.setMessage(errorCode);
+				builder.setNeutralButton(R.string.accept, null);
+				builder.create().show();
+			}
+		});
+
+	}
+
+	protected void showAddToRosterDialog(final Conversation conversation) {
+		String jid = conversation.getContactJid();
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(jid);
+		builder.setMessage(getString(R.string.not_in_roster));
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		builder.setPositiveButton(getString(R.string.add_contact),
+				new DialogInterface.OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						String jid = conversation.getContactJid();
+						Account account = conversation.getAccount();
+						Contact contact = account.getRoster().getContact(jid);
+						xmppConnectionService.createContact(contact);
+						switchToContactDetails(contact);
+					}
+				});
+		builder.create().show();
+	}
+
+	private void showAskForPresenceDialog(final Contact contact) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(contact.getJid());
+		builder.setMessage(R.string.request_presence_updates);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setPositiveButton(R.string.request_now,
+				new DialogInterface.OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						if (xmppConnectionServiceBound) {
+							xmppConnectionService.sendPresencePacket(contact
+									.getAccount(), xmppConnectionService
+									.getPresenceGenerator()
+									.requestPresenceUpdatesFrom(contact));
+						}
+					}
+				});
+		builder.create().show();
+	}
+
+	private void warnMutalPresenceSubscription(final Conversation conversation,
+			final OnPresenceSelected listener) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		builder.setTitle(conversation.getContact().getJid());
+		builder.setMessage(R.string.without_mutual_presence_updates);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setPositiveButton(R.string.ignore, new OnClickListener() {
+
+			@Override
+			public void onClick(DialogInterface dialog, int which) {
+				conversation.setNextPresence(null);
+				if (listener != null) {
+					listener.onPresenceSelected();
+				}
+			}
+		});
+		builder.create().show();
+	}
+
+	protected void quickEdit(String previousValue, OnValueEdited callback) {
+		quickEdit(previousValue, callback, false);
+	}
+
+	protected void quickPasswordEdit(String previousValue,
+			OnValueEdited callback) {
+		quickEdit(previousValue, callback, true);
+	}
+
+	@SuppressLint("InflateParams")
+	private void quickEdit(final String previousValue,
+			final OnValueEdited callback, boolean password) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		View view = (View) getLayoutInflater()
+				.inflate(R.layout.quickedit, null);
+		final EditText editor = (EditText) view.findViewById(R.id.editor);
+		OnClickListener mClickListener = new OnClickListener() {
+
+			@Override
+			public void onClick(DialogInterface dialog, int which) {
+				String value = editor.getText().toString();
+				if (!previousValue.equals(value) && value.trim().length() > 0) {
+					callback.onValueEdited(value);
+				}
+			}
+		};
+		if (password) {
+			editor.setInputType(InputType.TYPE_CLASS_TEXT
+					| InputType.TYPE_TEXT_VARIATION_PASSWORD);
+			editor.setHint(R.string.password);
+			builder.setPositiveButton(R.string.accept, mClickListener);
+		} else {
+			builder.setPositiveButton(R.string.edit, mClickListener);
+		}
+		editor.requestFocus();
+		editor.setText(previousValue);
+		builder.setView(view);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.create().show();
+	}
+
+	public void selectPresence(final Conversation conversation,
+			final OnPresenceSelected listener) {
+		Contact contact = conversation.getContact();
+		if (!contact.showInRoster()) {
+			showAddToRosterDialog(conversation);
+		} else {
+			Presences presences = contact.getPresences();
+			if (presences.size() == 0) {
+				if (!contact.getOption(Contact.Options.TO)
+						&& !contact.getOption(Contact.Options.ASKING)
+						&& contact.getAccount().getStatus() == Account.STATUS_ONLINE) {
+					showAskForPresenceDialog(contact);
+				} else if (!contact.getOption(Contact.Options.TO)
+						|| !contact.getOption(Contact.Options.FROM)) {
+					warnMutalPresenceSubscription(conversation, listener);
+				} else {
+					conversation.setNextPresence(null);
+					listener.onPresenceSelected();
+				}
+			} else if (presences.size() == 1) {
+				String presence = (String) presences.asStringArray()[0];
+				conversation.setNextPresence(presence);
+				listener.onPresenceSelected();
+			} else {
+				final StringBuilder presence = new StringBuilder();
+				AlertDialog.Builder builder = new AlertDialog.Builder(this);
+				builder.setTitle(getString(R.string.choose_presence));
+				final String[] presencesArray = presences.asStringArray();
+				int preselectedPresence = 0;
+				for (int i = 0; i < presencesArray.length; ++i) {
+					if (presencesArray[i].equals(contact.lastseen.presence)) {
+						preselectedPresence = i;
+						break;
+					}
+				}
+				presence.append(presencesArray[preselectedPresence]);
+				builder.setSingleChoiceItems(presencesArray,
+						preselectedPresence,
+						new DialogInterface.OnClickListener() {
+
+							@Override
+							public void onClick(DialogInterface dialog,
+									int which) {
+								presence.delete(0, presence.length());
+								presence.append(presencesArray[which]);
+							}
+						});
+				builder.setNegativeButton(R.string.cancel, null);
+				builder.setPositiveButton(R.string.ok, new OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						conversation.setNextPresence(presence.toString());
+						listener.onPresenceSelected();
+					}
+				});
+				builder.create().show();
+			}
+		}
+	}
+
+	protected void onActivityResult(int requestCode, int resultCode,
+			final Intent data) {
+		super.onActivityResult(requestCode, resultCode, data);
+		if (requestCode == REQUEST_INVITE_TO_CONVERSATION
+				&& resultCode == RESULT_OK) {
+			String contactJid = data.getStringExtra("contact");
+			String conversationUuid = data.getStringExtra("conversation");
+			Conversation conversation = xmppConnectionService
+					.findConversationByUuid(conversationUuid);
+			if (conversation.getMode() == Conversation.MODE_MULTI) {
+				xmppConnectionService.invite(conversation, contactJid);
+			}
+			Log.d(Config.LOGTAG, "inviting " + contactJid + " to "
+					+ conversation.getName());
+		}
+	}
+
+	public int getSecondaryTextColor() {
+		return this.mSecondaryTextColor;
+	}
+
+	public int getPrimaryTextColor() {
+		return this.mPrimaryTextColor;
+	}
+
+	public int getWarningTextColor() {
+		return this.mColorRed;
+	}
+
+	public int getPrimaryColor() {
+		return this.mPrimaryColor;
+	}
+
+	public int getSecondaryBackgroundColor() {
+		return this.mSecondaryBackgroundColor;
+	}
+
+	public int getPixel(int dp) {
+		DisplayMetrics metrics = getResources().getDisplayMetrics();
+		return ((int) (dp * metrics.density));
+	}
+
+	public AvatarService avatarService() {
+		return xmppConnectionService.getAvatarService();
+	}
+
+	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
+		private final WeakReference<ImageView> imageViewReference;
+		private Message message = null;
+
+		public BitmapWorkerTask(ImageView imageView) {
+			imageViewReference = new WeakReference<ImageView>(imageView);
+		}
+
+		@Override
+		protected Bitmap doInBackground(Message... params) {
+			message = params[0];
+			try {
+				return xmppConnectionService.getFileBackend().getThumbnail(
+						message, (int) (metrics.density * 288), false);
+			} catch (FileNotFoundException e) {
+				return null;
+			}
+		}
+
+		@Override
+		protected void onPostExecute(Bitmap bitmap) {
+			if (imageViewReference != null && bitmap != null) {
+				final ImageView imageView = imageViewReference.get();
+				if (imageView != null) {
+					imageView.setImageBitmap(bitmap);
+					imageView.setBackgroundColor(0x00000000);
+				}
+			}
+		}
+	}
+
+	public void loadBitmap(Message message, ImageView imageView) {
+		Bitmap bm;
+		try {
+			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
+					(int) (metrics.density * 288), true);
+		} catch (FileNotFoundException e) {
+			bm = null;
+		}
+		if (bm != null) {
+			imageView.setImageBitmap(bm);
+			imageView.setBackgroundColor(0x00000000);
+		} else {
+			if (cancelPotentialWork(message, imageView)) {
+				imageView.setBackgroundColor(0xff333333);
+				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+				final AsyncDrawable asyncDrawable = new AsyncDrawable(
+						getResources(), null, task);
+				imageView.setImageDrawable(asyncDrawable);
+				try {
+					task.execute(message);
+				} catch (RejectedExecutionException e) {
+					return;
+				}
+			}
+		}
+	}
+
+	public static boolean cancelPotentialWork(Message message,
+			ImageView imageView) {
+		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+		if (bitmapWorkerTask != null) {
+			final Message oldMessage = bitmapWorkerTask.message;
+			if (oldMessage == null || message != oldMessage) {
+				bitmapWorkerTask.cancel(true);
+			} else {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+		if (imageView != null) {
+			final Drawable drawable = imageView.getDrawable();
+			if (drawable instanceof AsyncDrawable) {
+				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+				return asyncDrawable.getBitmapWorkerTask();
+			}
+		}
+		return null;
+	}
+
+	static class AsyncDrawable extends BitmapDrawable {
+		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+		public AsyncDrawable(Resources res, Bitmap bitmap,
+				BitmapWorkerTask bitmapWorkerTask) {
+			super(res, bitmap);
+			bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
+					bitmapWorkerTask);
+		}
+
+		public BitmapWorkerTask getBitmapWorkerTask() {
+			return bitmapWorkerTaskReference.get();
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java 🔗

@@ -0,0 +1,102 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.XmppActivity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class AccountAdapter extends ArrayAdapter<Account> {
+
+	private XmppActivity activity;
+
+	public AccountAdapter(XmppActivity activity, List<Account> objects) {
+		super(activity, 0, objects);
+		this.activity = activity;
+	}
+
+	@Override
+	public View getView(int position, View view, ViewGroup parent) {
+		Account account = getItem(position);
+		if (view == null) {
+			LayoutInflater inflater = (LayoutInflater) getContext()
+					.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+			view = (View) inflater.inflate(R.layout.account_row, parent, false);
+		}
+		TextView jid = (TextView) view.findViewById(R.id.account_jid);
+		jid.setText(account.getJid());
+		TextView statusView = (TextView) view.findViewById(R.id.account_status);
+		ImageView imageView = (ImageView) view.findViewById(R.id.account_image);
+		imageView.setImageBitmap(activity.avatarService().get(account,
+				activity.getPixel(48)));
+		switch (account.getStatus()) {
+		case Account.STATUS_DISABLED:
+			statusView.setText(getContext().getString(
+					R.string.account_status_disabled));
+			statusView.setTextColor(activity.getSecondaryTextColor());
+			break;
+		case Account.STATUS_ONLINE:
+			statusView.setText(getContext().getString(
+					R.string.account_status_online));
+			statusView.setTextColor(activity.getPrimaryColor());
+			break;
+		case Account.STATUS_CONNECTING:
+			statusView.setText(getContext().getString(
+					R.string.account_status_connecting));
+			statusView.setTextColor(activity.getSecondaryTextColor());
+			break;
+		case Account.STATUS_OFFLINE:
+			statusView.setText(getContext().getString(
+					R.string.account_status_offline));
+			statusView.setTextColor(activity.getWarningTextColor());
+			break;
+		case Account.STATUS_UNAUTHORIZED:
+			statusView.setText(getContext().getString(
+					R.string.account_status_unauthorized));
+			statusView.setTextColor(activity.getWarningTextColor());
+			break;
+		case Account.STATUS_SERVER_NOT_FOUND:
+			statusView.setText(getContext().getString(
+					R.string.account_status_not_found));
+			statusView.setTextColor(activity.getWarningTextColor());
+			break;
+		case Account.STATUS_NO_INTERNET:
+			statusView.setText(getContext().getString(
+					R.string.account_status_no_internet));
+			statusView.setTextColor(activity.getWarningTextColor());
+			break;
+		case Account.STATUS_REGISTRATION_FAILED:
+			statusView.setText(getContext().getString(
+					R.string.account_status_regis_fail));
+			statusView.setTextColor(activity.getWarningTextColor());
+			break;
+		case Account.STATUS_REGISTRATION_CONFLICT:
+			statusView.setText(getContext().getString(
+					R.string.account_status_regis_conflict));
+			statusView.setTextColor(activity.getWarningTextColor());
+			break;
+		case Account.STATUS_REGISTRATION_SUCCESSFULL:
+			statusView.setText(getContext().getString(
+					R.string.account_status_regis_success));
+			statusView.setTextColor(activity.getSecondaryTextColor());
+			break;
+		case Account.STATUS_REGISTRATION_NOT_SUPPORTED:
+			statusView.setText(getContext().getString(
+					R.string.account_status_regis_not_sup));
+			statusView.setTextColor(activity.getWarningTextColor());
+			break;
+		default:
+			statusView.setText("");
+			break;
+		}
+
+		return view;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java 🔗

@@ -0,0 +1,135 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ConversationAdapter extends ArrayAdapter<Conversation> {
+
+	private XmppActivity activity;
+
+	public ConversationAdapter(XmppActivity activity,
+			List<Conversation> conversations) {
+		super(activity, 0, conversations);
+		this.activity = activity;
+	}
+
+	@Override
+	public View getView(int position, View view, ViewGroup parent) {
+		if (view == null) {
+			LayoutInflater inflater = (LayoutInflater) activity
+					.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+			view = (View) inflater.inflate(R.layout.conversation_list_row,
+					parent, false);
+		}
+		Conversation conversation = getItem(position);
+		if (this.activity instanceof ConversationActivity) {
+			ConversationActivity activity = (ConversationActivity) this.activity;
+			if (!activity.isConversationsOverviewHideable()) {
+				if (conversation == activity.getSelectedConversation()) {
+					view.setBackgroundColor(activity
+							.getSecondaryBackgroundColor());
+				} else {
+					view.setBackgroundColor(Color.TRANSPARENT);
+				}
+			} else {
+				view.setBackgroundColor(Color.TRANSPARENT);
+			}
+		}
+		TextView convName = (TextView) view
+				.findViewById(R.id.conversation_name);
+		if (conversation.getMode() == Conversation.MODE_SINGLE
+				|| activity.useSubjectToIdentifyConference()) {
+			convName.setText(conversation.getName());
+		} else {
+			convName.setText(conversation.getContactJid().split("/")[0]);
+		}
+		TextView mLastMessage = (TextView) view
+				.findViewById(R.id.conversation_lastmsg);
+		TextView mTimestamp = (TextView) view
+				.findViewById(R.id.conversation_lastupdate);
+		ImageView imagePreview = (ImageView) view
+				.findViewById(R.id.conversation_lastimage);
+
+		Message message = conversation.getLatestMessage();
+
+		if (!conversation.isRead()) {
+			convName.setTypeface(null, Typeface.BOLD);
+		} else {
+			convName.setTypeface(null, Typeface.NORMAL);
+		}
+
+		if (message.getType() == Message.TYPE_IMAGE
+				|| message.getDownloadable() != null) {
+			Downloadable d = message.getDownloadable();
+			if (d != null) {
+				mLastMessage.setVisibility(View.VISIBLE);
+				imagePreview.setVisibility(View.GONE);
+				if (conversation.isRead()) {
+					mLastMessage.setTypeface(null, Typeface.ITALIC);
+				} else {
+					mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC);
+				}
+				if (d.getStatus() == Downloadable.STATUS_CHECKING) {
+					mLastMessage.setText(R.string.checking_image);
+				} else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
+					mLastMessage.setText(R.string.receiving_image);
+				} else if (d.getStatus() == Downloadable.STATUS_OFFER) {
+					mLastMessage.setText(R.string.image_offered_for_download);
+				} else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
+					mLastMessage.setText(R.string.image_offered_for_download);
+				} else if (d.getStatus() == Downloadable.STATUS_DELETED) {
+					mLastMessage.setText(R.string.image_file_deleted);
+				} else {
+					mLastMessage.setText("");
+				}
+			} else {
+				mLastMessage.setVisibility(View.GONE);
+				imagePreview.setVisibility(View.VISIBLE);
+				activity.loadBitmap(message, imagePreview);
+			}
+		} else {
+			if ((message.getEncryption() != Message.ENCRYPTION_PGP)
+					&& (message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
+				String body = Config.PARSE_EMOTICONS ? UIHelper
+						.transformAsciiEmoticons(message.getBody()) : message
+						.getBody();
+				mLastMessage.setText(body);
+			} else {
+				mLastMessage.setText(R.string.encrypted_message_received);
+			}
+			if (!conversation.isRead()) {
+				mLastMessage.setTypeface(null, Typeface.BOLD);
+			} else {
+				mLastMessage.setTypeface(null, Typeface.NORMAL);
+			}
+			mLastMessage.setVisibility(View.VISIBLE);
+			imagePreview.setVisibility(View.GONE);
+		}
+		mTimestamp.setText(UIHelper.readableTimeDifference(getContext(),
+				conversation.getLatestMessage().getTimeSent()));
+
+		ImageView profilePicture = (ImageView) view
+				.findViewById(R.id.conversation_image);
+		profilePicture.setImageBitmap(activity.avatarService().get(
+				conversation, activity.getPixel(56)));
+
+		return view;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java 🔗

@@ -0,0 +1,74 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+
+public class KnownHostsAdapter extends ArrayAdapter<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java 🔗

@@ -0,0 +1,44 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.XmppActivity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ListItemAdapter extends ArrayAdapter<ListItem> {
+
+	protected XmppActivity activity;
+
+	public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
+		super(activity, 0, objects);
+		this.activity = activity;
+	}
+
+	@Override
+	public View getView(int position, View view, ViewGroup parent) {
+		LayoutInflater inflater = (LayoutInflater) getContext()
+				.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+		ListItem item = getItem(position);
+		if (view == null) {
+			view = (View) inflater.inflate(R.layout.contact, parent, false);
+		}
+		TextView name = (TextView) view.findViewById(R.id.contact_display_name);
+		TextView jid = (TextView) view.findViewById(R.id.contact_jid);
+		ImageView picture = (ImageView) view.findViewById(R.id.contact_photo);
+
+		jid.setText(item.getJid());
+		name.setText(item.getDisplayName());
+		picture.setImageBitmap(activity.avatarService().get(item,
+				activity.getPixel(48)));
+		return view;
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -0,0 +1,560 @@
+package eu.siacs.conversations.ui.adapter;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Message.ImageParams;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class MessageAdapter extends ArrayAdapter<Message> {
+
+	private static final int SENT = 0;
+	private static final int RECEIVED = 1;
+	private static final int STATUS = 2;
+	private static final int NULL = 3;
+
+	private ConversationActivity activity;
+
+	private DisplayMetrics metrics;
+
+	private OnContactPictureClicked mOnContactPictureClickedListener;
+	private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
+
+	public MessageAdapter(ConversationActivity activity, List<Message> messages) {
+		super(activity, 0, messages);
+		this.activity = activity;
+		metrics = getContext().getResources().getDisplayMetrics();
+	}
+
+	public void setOnContactPictureClicked(OnContactPictureClicked listener) {
+		this.mOnContactPictureClickedListener = listener;
+	}
+
+	public void setOnContactPictureLongClicked(
+			OnContactPictureLongClicked listener) {
+		this.mOnContactPictureLongClickedListener = listener;
+	}
+
+	@Override
+	public int getViewTypeCount() {
+		return 4;
+	}
+
+	@Override
+	public int getItemViewType(int position) {
+		if (getItem(position).wasMergedIntoPrevious()) {
+			return NULL;
+		} else if (getItem(position).getType() == Message.TYPE_STATUS) {
+			return STATUS;
+		} else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) {
+			return RECEIVED;
+		} else {
+			return SENT;
+		}
+	}
+
+	private void displayStatus(ViewHolder viewHolder, Message message) {
+		String filesize = null;
+		String info = null;
+		boolean error = false;
+		if (viewHolder.indicatorReceived != null) {
+			viewHolder.indicatorReceived.setVisibility(View.GONE);
+		}
+		boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
+				&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
+		if (message.getType() == Message.TYPE_IMAGE
+				|| message.getDownloadable() != null) {
+			ImageParams params = message.getImageParams();
+			if (params.size != 0) {
+				filesize = params.size / 1024 + " KB";
+			}
+		}
+		switch (message.getMergedStatus()) {
+		case Message.STATUS_WAITING:
+			info = getContext().getString(R.string.waiting);
+			break;
+		case Message.STATUS_UNSEND:
+			info = getContext().getString(R.string.sending);
+			break;
+		case Message.STATUS_OFFERED:
+			info = getContext().getString(R.string.offering);
+			break;
+		case Message.STATUS_SEND_RECEIVED:
+			if (activity.indicateReceived()) {
+				viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+			}
+			break;
+		case Message.STATUS_SEND_DISPLAYED:
+			if (activity.indicateReceived()) {
+				viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+			}
+			break;
+		case Message.STATUS_SEND_FAILED:
+			info = getContext().getString(R.string.send_failed);
+			error = true;
+			break;
+		case Message.STATUS_SEND_REJECTED:
+			info = getContext().getString(R.string.send_rejected);
+			error = true;
+			break;
+		default:
+			if (multiReceived) {
+				Contact contact = message.getContact();
+				if (contact != null) {
+					info = contact.getDisplayName();
+				} else {
+					if (message.getPresence() != null) {
+						info = message.getPresence();
+					} else {
+						info = message.getCounterpart();
+					}
+				}
+			}
+			break;
+		}
+		if (error) {
+			viewHolder.time.setTextColor(activity.getWarningTextColor());
+		} else {
+			viewHolder.time.setTextColor(activity.getSecondaryTextColor());
+		}
+		if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+			viewHolder.indicator.setVisibility(View.GONE);
+		} else {
+			viewHolder.indicator.setVisibility(View.VISIBLE);
+		}
+
+		String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
+				message.getMergedTimeSent());
+		if (message.getStatus() <= Message.STATUS_RECEIVED) {
+			if ((filesize != null) && (info != null)) {
+				viewHolder.time.setText(filesize + " \u00B7 " + info);
+			} else if ((filesize == null) && (info != null)) {
+				viewHolder.time.setText(formatedTime + " \u00B7 " + info);
+			} else if ((filesize != null) && (info == null)) {
+				viewHolder.time.setText(formatedTime + " \u00B7 " + filesize);
+			} else {
+				viewHolder.time.setText(formatedTime);
+			}
+		} else {
+			if ((filesize != null) && (info != null)) {
+				viewHolder.time.setText(filesize + " \u00B7 " + info);
+			} else if ((filesize == null) && (info != null)) {
+				if (error) {
+					viewHolder.time.setText(info + " \u00B7 " + formatedTime);
+				} else {
+					viewHolder.time.setText(info);
+				}
+			} else if ((filesize != null) && (info == null)) {
+				viewHolder.time.setText(filesize + " \u00B7 " + formatedTime);
+			} else {
+				viewHolder.time.setText(formatedTime);
+			}
+		}
+	}
+
+	private void displayInfoMessage(ViewHolder viewHolder, int r) {
+		if (viewHolder.download_button != null) {
+			viewHolder.download_button.setVisibility(View.GONE);
+		}
+		viewHolder.image.setVisibility(View.GONE);
+		viewHolder.messageBody.setVisibility(View.VISIBLE);
+		viewHolder.messageBody.setText(getContext().getString(r));
+		viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
+		viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
+		viewHolder.messageBody.setTextIsSelectable(false);
+	}
+
+	private void displayDecryptionFailed(ViewHolder viewHolder) {
+		if (viewHolder.download_button != null) {
+			viewHolder.download_button.setVisibility(View.GONE);
+		}
+		viewHolder.image.setVisibility(View.GONE);
+		viewHolder.messageBody.setVisibility(View.VISIBLE);
+		viewHolder.messageBody.setText(getContext().getString(
+				R.string.decryption_failed));
+		viewHolder.messageBody.setTextColor(activity.getWarningTextColor());
+		viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+		viewHolder.messageBody.setTextIsSelectable(false);
+	}
+
+	private void displayTextMessage(ViewHolder viewHolder, Message message) {
+		if (viewHolder.download_button != null) {
+			viewHolder.download_button.setVisibility(View.GONE);
+		}
+		viewHolder.image.setVisibility(View.GONE);
+		viewHolder.messageBody.setVisibility(View.VISIBLE);
+		if (message.getBody() != null) {
+			if (message.getType() != Message.TYPE_PRIVATE) {
+				String body = Config.PARSE_EMOTICONS ? UIHelper
+						.transformAsciiEmoticons(message.getMergedBody())
+						: message.getMergedBody();
+				viewHolder.messageBody.setText(body);
+			} else {
+				String privateMarker;
+				if (message.getStatus() <= Message.STATUS_RECEIVED) {
+					privateMarker = activity
+							.getString(R.string.private_message);
+				} else {
+					String to;
+					if (message.getPresence() != null) {
+						to = message.getPresence();
+					} else {
+						to = message.getCounterpart();
+					}
+					privateMarker = activity.getString(
+							R.string.private_message_to, to);
+				}
+				SpannableString span = new SpannableString(privateMarker + " "
+						+ message.getBody());
+				span.setSpan(
+						new ForegroundColorSpan(activity
+								.getSecondaryTextColor()), 0, privateMarker
+								.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+				span.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0,
+						privateMarker.length(),
+						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+				viewHolder.messageBody.setText(span);
+			}
+		} else {
+			viewHolder.messageBody.setText("");
+		}
+		viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor());
+		viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+		viewHolder.messageBody.setTextIsSelectable(true);
+	}
+
+	private void displayDownloadableMessage(ViewHolder viewHolder,
+			final Message message, int resid) {
+		viewHolder.image.setVisibility(View.GONE);
+		viewHolder.messageBody.setVisibility(View.GONE);
+		viewHolder.download_button.setVisibility(View.VISIBLE);
+		viewHolder.download_button.setText(resid);
+		viewHolder.download_button.setOnClickListener(new OnClickListener() {
+
+			@Override
+			public void onClick(View v) {
+				startDonwloadable(message);
+			}
+		});
+	}
+
+	private void displayImageMessage(ViewHolder viewHolder,
+			final Message message) {
+		if (viewHolder.download_button != null) {
+			viewHolder.download_button.setVisibility(View.GONE);
+		}
+		viewHolder.messageBody.setVisibility(View.GONE);
+		viewHolder.image.setVisibility(View.VISIBLE);
+		ImageParams params = message.getImageParams();
+		double target = metrics.density * 288;
+		int scalledW;
+		int scalledH;
+		if (params.width <= params.height) {
+			scalledW = (int) (params.width / ((double) params.height / target));
+			scalledH = (int) target;
+		} else {
+			scalledW = (int) target;
+			scalledH = (int) (params.height / ((double) params.width / target));
+		}
+		viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
+				scalledW, scalledH));
+		activity.loadBitmap(message, viewHolder.image);
+		viewHolder.image.setOnClickListener(new OnClickListener() {
+
+			@Override
+			public void onClick(View v) {
+				Intent intent = new Intent(Intent.ACTION_VIEW);
+				intent.setDataAndType(activity.xmppConnectionService
+						.getFileBackend().getJingleFileUri(message), "image/*");
+				getContext().startActivity(intent);
+			}
+		});
+		viewHolder.image.setOnLongClickListener(new OnLongClickListener() {
+
+			@Override
+			public boolean onLongClick(View v) {
+				Intent shareIntent = new Intent();
+				shareIntent.setAction(Intent.ACTION_SEND);
+				shareIntent.putExtra(Intent.EXTRA_STREAM,
+						activity.xmppConnectionService.getFileBackend()
+								.getJingleFileUri(message));
+				shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+				shareIntent.setType("image/webp");
+				getContext().startActivity(
+						Intent.createChooser(shareIntent,
+								getContext().getText(R.string.share_with)));
+				return true;
+			}
+		});
+	}
+
+	@Override
+	public View getView(int position, View view, ViewGroup parent) {
+		final Message item = getItem(position);
+		int type = getItemViewType(position);
+		ViewHolder viewHolder;
+		if (view == null) {
+			viewHolder = new ViewHolder();
+			switch (type) {
+			case NULL:
+				view = (View) activity.getLayoutInflater().inflate(
+						R.layout.message_null, parent, false);
+				break;
+			case SENT:
+				view = (View) activity.getLayoutInflater().inflate(
+						R.layout.message_sent, parent, false);
+				viewHolder.message_box = (LinearLayout) view
+						.findViewById(R.id.message_box);
+				viewHolder.contact_picture = (ImageView) view
+						.findViewById(R.id.message_photo);
+				viewHolder.contact_picture.setImageBitmap(activity
+						.avatarService().get(
+								item.getConversation().getAccount(),
+								activity.getPixel(48)));
+				viewHolder.download_button = (Button) view
+						.findViewById(R.id.download_button);
+				viewHolder.indicator = (ImageView) view
+						.findViewById(R.id.security_indicator);
+				viewHolder.image = (ImageView) view
+						.findViewById(R.id.message_image);
+				viewHolder.messageBody = (TextView) view
+						.findViewById(R.id.message_body);
+				viewHolder.time = (TextView) view
+						.findViewById(R.id.message_time);
+				viewHolder.indicatorReceived = (ImageView) view
+						.findViewById(R.id.indicator_received);
+				view.setTag(viewHolder);
+				break;
+			case RECEIVED:
+				view = (View) activity.getLayoutInflater().inflate(
+						R.layout.message_received, parent, false);
+				viewHolder.message_box = (LinearLayout) view
+						.findViewById(R.id.message_box);
+				viewHolder.contact_picture = (ImageView) view
+						.findViewById(R.id.message_photo);
+				viewHolder.download_button = (Button) view
+						.findViewById(R.id.download_button);
+				if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
+					viewHolder.contact_picture.setImageBitmap(activity
+							.avatarService().get(item.getContact(),
+									activity.getPixel(48)));
+				}
+				viewHolder.indicator = (ImageView) view
+						.findViewById(R.id.security_indicator);
+				viewHolder.image = (ImageView) view
+						.findViewById(R.id.message_image);
+				viewHolder.messageBody = (TextView) view
+						.findViewById(R.id.message_body);
+				viewHolder.time = (TextView) view
+						.findViewById(R.id.message_time);
+				view.setTag(viewHolder);
+				break;
+			case STATUS:
+				view = (View) activity.getLayoutInflater().inflate(
+						R.layout.message_status, parent, false);
+				viewHolder.contact_picture = (ImageView) view
+						.findViewById(R.id.message_photo);
+				if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
+
+					viewHolder.contact_picture.setImageBitmap(activity
+							.avatarService().get(
+									item.getConversation().getContact(),
+									activity.getPixel(32)));
+					viewHolder.contact_picture.setAlpha(0.5f);
+					viewHolder.contact_picture
+							.setOnClickListener(new OnClickListener() {
+
+								@Override
+								public void onClick(View v) {
+									String name = item.getConversation()
+											.getName();
+									String read = getContext()
+											.getString(
+													R.string.contact_has_read_up_to_this_point,
+													name);
+									Toast.makeText(getContext(), read,
+											Toast.LENGTH_SHORT).show();
+								}
+							});
+
+				}
+				break;
+			default:
+				viewHolder = null;
+				break;
+			}
+		} else {
+			viewHolder = (ViewHolder) view.getTag();
+		}
+
+		if (type == STATUS) {
+			return view;
+		}
+		if (type == NULL) {
+			if (position == getCount() - 1) {
+				view.getLayoutParams().height = 1;
+			} else {
+				view.getLayoutParams().height = 0;
+
+			}
+			view.setLayoutParams(view.getLayoutParams());
+			return view;
+		}
+
+		if (viewHolder.contact_picture != null) {
+			viewHolder.contact_picture
+					.setOnClickListener(new OnClickListener() {
+
+						@Override
+						public void onClick(View v) {
+							if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
+								MessageAdapter.this.mOnContactPictureClickedListener
+										.onContactPictureClicked(item);
+								;
+							}
+
+						}
+					});
+			viewHolder.contact_picture
+					.setOnLongClickListener(new OnLongClickListener() {
+
+						@Override
+						public boolean onLongClick(View v) {
+							if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
+								MessageAdapter.this.mOnContactPictureLongClickedListener
+										.onContactPictureLongClicked(item);
+								return true;
+							} else {
+								return false;
+							}
+						}
+					});
+		}
+
+		if (type == RECEIVED) {
+			if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
+				Contact contact = item.getContact();
+				if (contact != null) {
+					viewHolder.contact_picture.setImageBitmap(activity
+							.avatarService()
+							.get(contact, activity.getPixel(48)));
+				} else {
+					String name = item.getPresence();
+					if (name == null) {
+						name = item.getCounterpart();
+					}
+					viewHolder.contact_picture.setImageBitmap(activity
+							.avatarService().get(name, activity.getPixel(48)));
+				}
+			}
+		}
+
+		if (item.getType() == Message.TYPE_IMAGE
+				|| item.getDownloadable() != null) {
+			Downloadable d = item.getDownloadable();
+			if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
+				displayInfoMessage(viewHolder, R.string.receiving_image);
+			} else if (d != null
+					&& d.getStatus() == Downloadable.STATUS_CHECKING) {
+				displayInfoMessage(viewHolder, R.string.checking_image);
+			} else if (d != null
+					&& d.getStatus() == Downloadable.STATUS_DELETED) {
+				displayInfoMessage(viewHolder, R.string.image_file_deleted);
+			} else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) {
+				displayDownloadableMessage(viewHolder, item,
+						R.string.download_image);
+			} else if (d != null
+					&& d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
+				displayDownloadableMessage(viewHolder, item,
+						R.string.check_image_filesize);
+			} else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED)
+					|| (item.getEncryption() == Message.ENCRYPTION_NONE)
+					|| (item.getEncryption() == Message.ENCRYPTION_OTR)) {
+				displayImageMessage(viewHolder, item);
+			} else if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+				displayInfoMessage(viewHolder, R.string.encrypted_message);
+			} else {
+				displayDecryptionFailed(viewHolder);
+			}
+		} else {
+			if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+				if (activity.hasPgp()) {
+					displayInfoMessage(viewHolder, R.string.encrypted_message);
+				} else {
+					displayInfoMessage(viewHolder,
+							R.string.install_openkeychain);
+					viewHolder.message_box
+							.setOnClickListener(new OnClickListener() {
+
+								@Override
+								public void onClick(View v) {
+									activity.showInstallPgpDialog();
+								}
+							});
+				}
+			} else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+				displayDecryptionFailed(viewHolder);
+			} else {
+				displayTextMessage(viewHolder, item);
+			}
+		}
+
+		displayStatus(viewHolder, item);
+
+		return view;
+	}
+
+	public void startDonwloadable(Message message) {
+		Downloadable downloadable = message.getDownloadable();
+		if (downloadable != null) {
+			if (!downloadable.start()) {
+				Toast.makeText(activity, R.string.not_connected_try_again,
+						Toast.LENGTH_SHORT).show();
+			}
+		}
+	}
+
+	private static class ViewHolder {
+
+		protected LinearLayout message_box;
+		protected Button download_button;
+		protected ImageView image;
+		protected ImageView indicator;
+		protected ImageView indicatorReceived;
+		protected TextView time;
+		protected TextView messageBody;
+		protected ImageView contact_picture;
+
+	}
+
+	public interface OnContactPictureClicked {
+		public void onContactPictureClicked(Message message);
+	}
+
+	public interface OnContactPictureLongClicked {
+		public void onContactPictureLongClicked(Message message);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java 🔗

@@ -0,0 +1,112 @@
+package eu.siacs.conversations.utils;
+
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.entities.Account;
+import android.util.Base64;
+
+public class CryptoHelper {
+	public static final String FILETRANSFER = "?FILETRANSFERv1:";
+	final protected static char[] hexArray = "0123456789abcdef".toCharArray();
+	final protected static char[] vowels = "aeiou".toCharArray();
+	final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz"
+			.toCharArray();
+
+	public static String bytesToHex(byte[] bytes) {
+		char[] hexChars = new char[bytes.length * 2];
+		for (int j = 0; j < bytes.length; j++) {
+			int v = bytes[j] & 0xFF;
+			hexChars[j * 2] = hexArray[v >>> 4];
+			hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+		}
+		return new String(hexChars);
+	}
+
+	public static byte[] hexToBytes(String hexString) {
+		int len = hexString.length();
+		byte[] array = new byte[len / 2];
+		for (int i = 0; i < len; i += 2) {
+			array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
+					.digit(hexString.charAt(i + 1), 16));
+		}
+		return array;
+	}
+
+	public static String saslPlain(String username, String password) {
+		String sasl = '\u0000' + username + '\u0000' + password;
+		return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()),
+				Base64.NO_WRAP);
+	}
+
+	private static byte[] concatenateByteArrays(byte[] a, byte[] b) {
+		byte[] result = new byte[a.length + b.length];
+		System.arraycopy(a, 0, result, 0, a.length);
+		System.arraycopy(b, 0, result, a.length, b.length);
+		return result;
+	}
+
+	public static String saslDigestMd5(Account account, String challenge,
+			SecureRandom random) {
+		try {
+			String[] challengeParts = new String(Base64.decode(challenge,
+					Base64.DEFAULT)).split(",");
+			String nonce = "";
+			for (int i = 0; i < challengeParts.length; ++i) {
+				String[] parts = challengeParts[i].split("=");
+				if (parts[0].equals("nonce")) {
+					nonce = parts[1].replace("\"", "");
+				} else if (parts[0].equals("rspauth")) {
+					return null;
+				}
+			}
+			String digestUri = "xmpp/" + account.getServer();
+			String nonceCount = "00000001";
+			String x = account.getUsername() + ":" + account.getServer() + ":"
+					+ account.getPassword();
+			MessageDigest md = MessageDigest.getInstance("MD5");
+			byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
+			String cNonce = new BigInteger(100, random).toString(32);
+			byte[] a1 = concatenateByteArrays(y,
+					(":" + nonce + ":" + cNonce).getBytes(Charset
+							.defaultCharset()));
+			String a2 = "AUTHENTICATE:" + digestUri;
+			String ha1 = bytesToHex(md.digest(a1));
+			String ha2 = bytesToHex(md.digest(a2.getBytes(Charset
+					.defaultCharset())));
+			String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
+					+ ":auth:" + ha2;
+			String response = bytesToHex(md.digest(kd.getBytes(Charset
+					.defaultCharset())));
+			String saslString = "username=\"" + account.getUsername()
+					+ "\",realm=\"" + account.getServer() + "\",nonce=\""
+					+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
+					+ ",qop=auth,digest-uri=\"" + digestUri + "\",response="
+					+ response + ",charset=utf-8";
+			return Base64.encodeToString(
+					saslString.getBytes(Charset.defaultCharset()),
+					Base64.NO_WRAP);
+		} catch (NoSuchAlgorithmException e) {
+			return null;
+		}
+	}
+
+	public static String randomMucName(SecureRandom random) {
+		return randomWord(3, random) + "." + randomWord(7, random);
+	}
+
+	protected static String randomWord(int lenght, SecureRandom random) {
+		StringBuilder builder = new StringBuilder(lenght);
+		for (int i = 0; i < lenght; ++i) {
+			if (i % 2 == 0) {
+				builder.append(consonants[random.nextInt(consonants.length)]);
+			} else {
+				builder.append(vowels[random.nextInt(vowels.length)]);
+			}
+		}
+		return builder.toString();
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java 🔗

@@ -0,0 +1,185 @@
+package eu.siacs.conversations.utils;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.record.A;
+import de.measite.minidns.record.AAAA;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.util.NameUtil;
+import eu.siacs.conversations.Config;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Random;
+import java.util.TreeMap;
+
+import android.os.Bundle;
+import android.util.Log;
+
+public class DNSHelper {
+	protected static Client client = new Client();
+
+	public static Bundle getSRVRecord(String host) throws IOException {
+		String dns[] = client.findDNS();
+
+		if (dns != null) {
+			for (String dnsserver : dns) {
+				InetAddress ip = InetAddress.getByName(dnsserver);
+				Bundle b = queryDNS(host, ip);
+				if (b.containsKey("name")) {
+					return b;
+				} else if (b.containsKey("error")
+						&& "nosrv".equals(b.getString("error", null))) {
+					return b;
+				}
+			}
+		}
+		return queryDNS(host, InetAddress.getByName("8.8.8.8"));
+	}
+
+	public static Bundle queryDNS(String host, InetAddress dnsServer) {
+		Bundle namePort = new Bundle();
+		try {
+			String qname = "_xmpp-client._tcp." + host;
+			Log.d(Config.LOGTAG,
+					"using dns server: " + dnsServer.getHostAddress()
+							+ " to look up " + host);
+			DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN,
+					dnsServer.getHostAddress());
+
+			// How should we handle priorities and weight?
+			// Wikipedia has a nice article about priorities vs. weights:
+			// https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability
+
+			// we bucket the SRV records based on priority, pick per priority
+			// a random order respecting the weight, and dump that priority by
+			// priority
+
+			TreeMap<Integer, ArrayList<SRV>> priorities = new TreeMap<Integer, ArrayList<SRV>>();
+			TreeMap<String, ArrayList<String>> ips4 = new TreeMap<String, ArrayList<String>>();
+			TreeMap<String, ArrayList<String>> ips6 = new TreeMap<String, ArrayList<String>>();
+
+			for (Record[] rrset : new Record[][] { message.getAnswers(),
+					message.getAdditionalResourceRecords() }) {
+				for (Record rr : rrset) {
+					Data d = rr.getPayload();
+					if (d instanceof SRV
+							&& NameUtil.idnEquals(qname, rr.getName())) {
+						SRV srv = (SRV) d;
+						if (!priorities.containsKey(srv.getPriority())) {
+							priorities.put(srv.getPriority(),
+									new ArrayList<SRV>(2));
+						}
+						priorities.get(srv.getPriority()).add(srv);
+					}
+					if (d instanceof A) {
+						A arecord = (A) d;
+						if (!ips4.containsKey(rr.getName())) {
+							ips4.put(rr.getName(), new ArrayList<String>(3));
+						}
+						ips4.get(rr.getName()).add(arecord.toString());
+					}
+					if (d instanceof AAAA) {
+						AAAA aaaa = (AAAA) d;
+						if (!ips6.containsKey(rr.getName())) {
+							ips6.put(rr.getName(), new ArrayList<String>(3));
+						}
+						ips6.get(rr.getName()).add("[" + aaaa.toString() + "]");
+					}
+				}
+			}
+
+			Random rnd = new Random();
+			ArrayList<SRV> result = new ArrayList<SRV>(
+					priorities.size() * 2 + 1);
+			for (ArrayList<SRV> s : priorities.values()) {
+
+				// trivial case
+				if (s.size() <= 1) {
+					result.addAll(s);
+					continue;
+				}
+
+				long totalweight = 0l;
+				for (SRV srv : s) {
+					totalweight += srv.getWeight();
+				}
+
+				while (totalweight > 0l && s.size() > 0) {
+					long p = (rnd.nextLong() & 0x7fffffffffffffffl)
+							% totalweight;
+					int i = 0;
+					while (p > 0) {
+						p -= s.get(i++).getPriority();
+					}
+					i--;
+					// remove is expensive, but we have only a few entries
+					// anyway
+					SRV srv = s.remove(i);
+					totalweight -= srv.getWeight();
+					result.add(srv);
+				}
+
+				Collections.shuffle(s, rnd);
+				result.addAll(s);
+
+			}
+
+			if (result.size() == 0) {
+				namePort.putString("error", "nosrv");
+				return namePort;
+			}
+			// we now have a list of servers to try :-)
+
+			// classic name/port pair
+			String resultName = result.get(0).getName();
+			namePort.putString("name", resultName);
+			namePort.putInt("port", result.get(0).getPort());
+
+			if (ips4.containsKey(resultName)) {
+				// we have an ip!
+				ArrayList<String> ip = ips4.get(resultName);
+				Collections.shuffle(ip, rnd);
+				namePort.putString("ipv4", ip.get(0));
+			}
+			if (ips6.containsKey(resultName)) {
+				ArrayList<String> ip = ips6.get(resultName);
+				Collections.shuffle(ip, rnd);
+				namePort.putString("ipv6", ip.get(0));
+			}
+
+			// add all other records
+			int i = 0;
+			for (SRV srv : result) {
+				namePort.putString("name" + i, srv.getName());
+				namePort.putInt("port" + i, srv.getPort());
+				i++;
+			}
+
+		} catch (SocketTimeoutException e) {
+			namePort.putString("error", "timeout");
+		} catch (Exception e) {
+			namePort.putString("error", "unhandled");
+		}
+		return namePort;
+	}
+
+	final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+	public static String bytesToHex(byte[] bytes) {
+		char[] hexChars = new char[bytes.length * 2];
+		for (int j = 0; j < bytes.length; j++) {
+			int v = bytes[j] & 0xFF;
+			hexChars[j * 2] = hexArray[v >>> 4];
+			hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+		}
+		return new String(hexChars);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java 🔗

@@ -0,0 +1,44 @@
+package eu.siacs.conversations.utils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.Thread.UncaughtExceptionHandler;
+
+import android.content.Context;
+
+public class ExceptionHandler implements UncaughtExceptionHandler {
+
+	private UncaughtExceptionHandler defaultHandler;
+	private Context context;
+
+	public ExceptionHandler(Context context) {
+		this.context = context;
+		this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+	}
+
+	@Override
+	public void uncaughtException(Thread thread, Throwable ex) {
+		Writer result = new StringWriter();
+		PrintWriter printWriter = new PrintWriter(result);
+		ex.printStackTrace(printWriter);
+		String stacktrace = result.toString();
+		printWriter.close();
+		try {
+			OutputStream os = context.openFileOutput("stacktrace.txt",
+					Context.MODE_PRIVATE);
+			os.write(stacktrace.getBytes());
+		} catch (FileNotFoundException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (IOException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		this.defaultHandler.uncaughtException(thread, ex);
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java 🔗

@@ -0,0 +1,117 @@
+package eu.siacs.conversations.utils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnClickListener;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.preference.PreferenceManager;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+public class ExceptionHelper {
+	public static void init(Context context) {
+		if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {
+			Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(
+					context));
+		}
+	}
+
+	public static void checkForCrash(Context context,
+			final XmppConnectionService service) {
+		try {
+			final SharedPreferences preferences = PreferenceManager
+					.getDefaultSharedPreferences(context);
+			boolean neverSend = preferences.getBoolean("never_send", false);
+			if (neverSend) {
+				return;
+			}
+			List<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;
+		}
+
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java 🔗

@@ -0,0 +1,327 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ * 
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+
+	private static final int VERSION_CODE_JELLY_BEAN = 16;
+	private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+	private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial();
+
+	/** Hidden constructor to prevent instantiation. */
+	private PRNGFixes() {
+	}
+
+	/**
+	 * Applies all fixes.
+	 * 
+	 * @throws SecurityException
+	 *             if a fix is needed but could not be applied.
+	 */
+	public static void apply() {
+		applyOpenSSLFix();
+		installLinuxPRNGSecureRandom();
+	}
+
+	/**
+	 * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+	 * fix is not needed.
+	 * 
+	 * @throws SecurityException
+	 *             if the fix is needed but could not be applied.
+	 */
+	private static void applyOpenSSLFix() throws SecurityException {
+		if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+				|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+			// No need to apply the fix
+			return;
+		}
+
+		try {
+			// Mix in the device- and invocation-specific seed.
+			Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+					.getMethod("RAND_seed", byte[].class)
+					.invoke(null, generateSeed());
+
+			// Mix output of Linux PRNG into OpenSSL's PRNG
+			int bytesRead = (Integer) Class
+					.forName(
+							"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+					.getMethod("RAND_load_file", String.class, long.class)
+					.invoke(null, "/dev/urandom", 1024);
+			if (bytesRead != 1024) {
+				throw new IOException(
+						"Unexpected number of bytes read from Linux PRNG: "
+								+ bytesRead);
+			}
+		} catch (Exception e) {
+			throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+		}
+	}
+
+	/**
+	 * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+	 * default. Does nothing if the implementation is already the default or if
+	 * there is not need to install the implementation.
+	 * 
+	 * @throws SecurityException
+	 *             if the fix is needed but could not be applied.
+	 */
+	private static void installLinuxPRNGSecureRandom() throws SecurityException {
+		if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+			// No need to apply the fix
+			return;
+		}
+
+		// Install a Linux PRNG-based SecureRandom implementation as the
+		// default, if not yet installed.
+		Provider[] secureRandomProviders = Security
+				.getProviders("SecureRandom.SHA1PRNG");
+		if ((secureRandomProviders == null)
+				|| (secureRandomProviders.length < 1)
+				|| (!LinuxPRNGSecureRandomProvider.class
+						.equals(secureRandomProviders[0].getClass()))) {
+			Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+		}
+
+		// Assert that new SecureRandom() and
+		// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+		// by the Linux PRNG-based SecureRandom implementation.
+		SecureRandom rng1 = new SecureRandom();
+		if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider()
+				.getClass())) {
+			throw new SecurityException(
+					"new SecureRandom() backed by wrong Provider: "
+							+ rng1.getProvider().getClass());
+		}
+
+		SecureRandom rng2;
+		try {
+			rng2 = SecureRandom.getInstance("SHA1PRNG");
+		} catch (NoSuchAlgorithmException e) {
+			throw new SecurityException("SHA1PRNG not available", e);
+		}
+		if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider()
+				.getClass())) {
+			throw new SecurityException(
+					"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+							+ " Provider: " + rng2.getProvider().getClass());
+		}
+	}
+
+	/**
+	 * {@code Provider} of {@code SecureRandom} engines which pass through all
+	 * requests to the Linux PRNG.
+	 */
+	private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+		public LinuxPRNGSecureRandomProvider() {
+			super("LinuxPRNG", 1.0,
+					"A Linux-specific random number provider that uses"
+							+ " /dev/urandom");
+			// Although /dev/urandom is not a SHA-1 PRNG, some apps
+			// explicitly request a SHA1PRNG SecureRandom and we thus need to
+			// prevent them from getting the default implementation whose output
+			// may have low entropy.
+			put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+			put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+		}
+	}
+
+	/**
+	 * {@link SecureRandomSpi} which passes all requests to the Linux PRNG (
+	 * {@code /dev/urandom}).
+	 */
+	public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+		/*
+		 * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+		 * are passed through to the Linux PRNG (/dev/urandom). Instances of
+		 * this class seed themselves by mixing in the current time, PID, UID,
+		 * build fingerprint, and hardware serial number (where available) into
+		 * Linux PRNG.
+		 * 
+		 * Concurrency: Read requests to the underlying Linux PRNG are
+		 * serialized (on sLock) to ensure that multiple threads do not get
+		 * duplicated PRNG output.
+		 */
+
+		private static final File URANDOM_FILE = new File("/dev/urandom");
+
+		private static final Object sLock = new Object();
+
+		/**
+		 * Input stream for reading from Linux PRNG or {@code null} if not yet
+		 * opened.
+		 * 
+		 * @GuardedBy("sLock")
+		 */
+		private static DataInputStream sUrandomIn;
+
+		/**
+		 * Output stream for writing to Linux PRNG or {@code null} if not yet
+		 * opened.
+		 * 
+		 * @GuardedBy("sLock")
+		 */
+		private static OutputStream sUrandomOut;
+
+		/**
+		 * Whether this engine instance has been seeded. This is needed because
+		 * each instance needs to seed itself if the client does not explicitly
+		 * seed it.
+		 */
+		private boolean mSeeded;
+
+		@Override
+		protected void engineSetSeed(byte[] bytes) {
+			try {
+				OutputStream out;
+				synchronized (sLock) {
+					out = getUrandomOutputStream();
+				}
+				out.write(bytes);
+				out.flush();
+			} catch (IOException e) {
+				// On a small fraction of devices /dev/urandom is not writable.
+				// Log and ignore.
+				Log.w(PRNGFixes.class.getSimpleName(),
+						"Failed to mix seed into " + URANDOM_FILE);
+			} finally {
+				mSeeded = true;
+			}
+		}
+
+		@Override
+		protected void engineNextBytes(byte[] bytes) {
+			if (!mSeeded) {
+				// Mix in the device- and invocation-specific seed.
+				engineSetSeed(generateSeed());
+			}
+
+			try {
+				DataInputStream in;
+				synchronized (sLock) {
+					in = getUrandomInputStream();
+				}
+				synchronized (in) {
+					in.readFully(bytes);
+				}
+			} catch (IOException e) {
+				throw new SecurityException("Failed to read from "
+						+ URANDOM_FILE, e);
+			}
+		}
+
+		@Override
+		protected byte[] engineGenerateSeed(int size) {
+			byte[] seed = new byte[size];
+			engineNextBytes(seed);
+			return seed;
+		}
+
+		private DataInputStream getUrandomInputStream() {
+			synchronized (sLock) {
+				if (sUrandomIn == null) {
+					// NOTE: Consider inserting a BufferedInputStream between
+					// DataInputStream and FileInputStream if you need higher
+					// PRNG output performance and can live with future PRNG
+					// output being pulled into this process prematurely.
+					try {
+						sUrandomIn = new DataInputStream(new FileInputStream(
+								URANDOM_FILE));
+					} catch (IOException e) {
+						throw new SecurityException("Failed to open "
+								+ URANDOM_FILE + " for reading", e);
+					}
+				}
+				return sUrandomIn;
+			}
+		}
+
+		private OutputStream getUrandomOutputStream() throws IOException {
+			synchronized (sLock) {
+				if (sUrandomOut == null) {
+					sUrandomOut = new FileOutputStream(URANDOM_FILE);
+				}
+				return sUrandomOut;
+			}
+		}
+	}
+
+	/**
+	 * Generates a device- and invocation-specific seed to be mixed into the
+	 * Linux PRNG.
+	 */
+	private static byte[] generateSeed() {
+		try {
+			ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+			DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer);
+			seedBufferOut.writeLong(System.currentTimeMillis());
+			seedBufferOut.writeLong(System.nanoTime());
+			seedBufferOut.writeInt(Process.myPid());
+			seedBufferOut.writeInt(Process.myUid());
+			seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+			seedBufferOut.close();
+			return seedBuffer.toByteArray();
+		} catch (IOException e) {
+			throw new SecurityException("Failed to generate seed", e);
+		}
+	}
+
+	/**
+	 * Gets the hardware serial number of this device.
+	 * 
+	 * @return serial number or {@code null} if not available.
+	 */
+	private static String getDeviceSerialNumber() {
+		// We're using the Reflection API because Build.SERIAL is only available
+		// since API Level 9 (Gingerbread, Android 2.3).
+		try {
+			return (String) Build.class.getField("SERIAL").get(null);
+		} catch (Exception ignored) {
+			return null;
+		}
+	}
+
+	private static byte[] getBuildFingerprintAndDeviceSerial() {
+		StringBuilder result = new StringBuilder();
+		String fingerprint = Build.FINGERPRINT;
+		if (fingerprint != null) {
+			result.append(fingerprint);
+		}
+		String serial = getDeviceSerialNumber();
+		if (serial != null) {
+			result.append(serial);
+		}
+		try {
+			return result.toString().getBytes("UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			throw new RuntimeException("UTF-8 encoding not supported");
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java 🔗

@@ -0,0 +1,95 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Profile;
+
+public class PhoneHelper {
+
+	public static void loadPhoneContacts(Context context,
+			final OnPhoneContactsLoadedListener listener) {
+		final List<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);
+			}
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -0,0 +1,225 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class UIHelper {
+	private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
+			| DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
+	private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME
+			| DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
+
+	public static String readableTimeDifference(Context context, long time) {
+		return readableTimeDifference(context, time, false);
+	}
+
+	public static String readableTimeDifferenceFull(Context context, long time) {
+		return readableTimeDifference(context, time, true);
+	}
+
+	private static String readableTimeDifference(Context context, long time,
+			boolean fullDate) {
+		if (time == 0) {
+			return context.getString(R.string.just_now);
+		}
+		Date date = new Date(time);
+		long difference = (System.currentTimeMillis() - time) / 1000;
+		if (difference < 60) {
+			return context.getString(R.string.just_now);
+		} else if (difference < 60 * 2) {
+			return context.getString(R.string.minute_ago);
+		} else if (difference < 60 * 15) {
+			return context.getString(R.string.minutes_ago,
+					Math.round(difference / 60.0));
+		} else if (today(date)) {
+			java.text.DateFormat df = DateFormat.getTimeFormat(context);
+			return df.format(date);
+		} else {
+			if (fullDate) {
+				return DateUtils.formatDateTime(context, date.getTime(),
+						FULL_DATE_FLAGS);
+			} else {
+				return DateUtils.formatDateTime(context, date.getTime(),
+						SHORT_DATE_FLAGS);
+			}
+		}
+	}
+
+	private static boolean today(Date date) {
+		Calendar cal1 = Calendar.getInstance();
+		Calendar cal2 = Calendar.getInstance();
+		cal1.setTime(date);
+		cal2.setTimeInMillis(System.currentTimeMillis());
+		return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
+				&& cal1.get(Calendar.DAY_OF_YEAR) == cal2
+						.get(Calendar.DAY_OF_YEAR);
+	}
+
+	public static String lastseen(Context context, long time) {
+		if (time == 0) {
+			return context.getString(R.string.never_seen);
+		}
+		long difference = (System.currentTimeMillis() - time) / 1000;
+		if (difference < 60) {
+			return context.getString(R.string.last_seen_now);
+		} else if (difference < 60 * 2) {
+			return context.getString(R.string.last_seen_min);
+		} else if (difference < 60 * 60) {
+			return context.getString(R.string.last_seen_mins,
+					Math.round(difference / 60.0));
+		} else if (difference < 60 * 60 * 2) {
+			return context.getString(R.string.last_seen_hour);
+		} else if (difference < 60 * 60 * 24) {
+			return context.getString(R.string.last_seen_hours,
+					Math.round(difference / (60.0 * 60.0)));
+		} else if (difference < 60 * 60 * 48) {
+			return context.getString(R.string.last_seen_day);
+		} else {
+			return context.getString(R.string.last_seen_days,
+					Math.round(difference / (60.0 * 60.0 * 24.0)));
+		}
+	}
+
+	public static void showErrorNotification(Context context,
+			List<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/Validator.java 🔗

@@ -0,0 +1,14 @@
+package eu.siacs.conversations.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Validator {
+	public static final Pattern VALID_JID = Pattern.compile(
+			"^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE);
+
+	public static boolean isValidJid(String jid) {
+		Matcher matcher = VALID_JID.matcher(jid);
+		return matcher.find();
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java 🔗

@@ -0,0 +1,12 @@
+package eu.siacs.conversations.utils;
+
+public class XmlHelper {
+	public static String encodeEntities(String content) {
+		content = content.replace("&", "&amp;");
+		content = content.replace("<", "&lt;");
+		content = content.replace(">", "&gt;");
+		content = content.replace("\"", "&quot;");
+		content = content.replace("'", "&apos;");
+		return content;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java 🔗

@@ -0,0 +1,54 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * ZLibInputStream is a zlib and input stream compatible version of an
+ * InflaterInputStream. This class solves the incompatibility between
+ * {@link InputStream#available()} and {@link InflaterInputStream#available()}.
+ */
+public class ZLibInputStream extends InflaterInputStream {
+
+	/**
+	 * Construct a ZLibInputStream, reading data from the underlying stream.
+	 * 
+	 * @param is
+	 *            The {@code InputStream} to read data from.
+	 * @throws IOException
+	 *             If an {@code IOException} occurs.
+	 */
+	public ZLibInputStream(InputStream is) throws IOException {
+		super(is, new Inflater(), 512);
+	}
+
+	/**
+	 * Provide a more InputStream compatible version of available. A return
+	 * value of 1 means that it is likly to read one byte without blocking, 0
+	 * means that the system is known to block for more input.
+	 * 
+	 * @return 0 if no data is available, 1 otherwise
+	 * @throws IOException
+	 */
+	@Override
+	public int available() throws IOException {
+		/*
+		 * This is one of the funny code blocks. InflaterInputStream.available
+		 * violates the contract of InputStream.available, which breaks kXML2.
+		 * 
+		 * I'm not sure who's to blame, oracle/sun for a broken api or the
+		 * google guys for mixing a sun bug with a xml reader that can't handle
+		 * it....
+		 * 
+		 * Anyway, this simple if breaks suns distorted reality, but helps to
+		 * use the api as intended.
+		 */
+		if (inf.needsInput()) {
+			return 0;
+		}
+		return super.available();
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java 🔗

@@ -0,0 +1,95 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.NoSuchAlgorithmException;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+/**
+ * <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();
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/xml/Element.java 🔗

@@ -0,0 +1,148 @@
+package eu.siacs.conversations.xml;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+import eu.siacs.conversations.utils.XmlHelper;
+
+public class Element {
+	protected String name;
+	protected Hashtable<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));
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xml/Tag.java 🔗

@@ -0,0 +1,104 @@
+package eu.siacs.conversations.xml;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import eu.siacs.conversations.utils.XmlHelper;
+
+public class Tag {
+	public static final int NO = -1;
+	public static final int START = 0;
+	public static final int END = 1;
+	public static final int EMPTY = 2;
+
+	protected int type;
+	protected String name;
+	protected Hashtable<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java 🔗

@@ -0,0 +1,114 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class TagWriter {
+
+	private OutputStream plainOutputStream;
+	private OutputStreamWriter outputStream;
+	private boolean finshed = false;
+	private LinkedBlockingQueue<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;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java 🔗

@@ -0,0 +1,141 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import eu.siacs.conversations.Config;
+
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
+
+public class XmlReader {
+	private XmlPullParser parser;
+	private PowerManager.WakeLock wakeLock;
+	private InputStream is;
+
+	public XmlReader(WakeLock wakeLock) {
+		this.parser = Xml.newPullParser();
+		try {
+			this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
+					true);
+		} catch (XmlPullParserException e) {
+			Log.d(Config.LOGTAG, "error setting namespace feature on parser");
+		}
+		this.wakeLock = wakeLock;
+	}
+
+	public void setInputStream(InputStream inputStream) throws IOException {
+		if (inputStream == null) {
+			throw new IOException();
+		}
+		this.is = inputStream;
+		try {
+			parser.setInput(new InputStreamReader(this.is));
+		} catch (XmlPullParserException e) {
+			throw new IOException("error resetting parser");
+		}
+	}
+
+	public InputStream getInputStream() throws IOException {
+		if (this.is == null) {
+			throw new IOException();
+		}
+		return is;
+	}
+
+	public void reset() throws IOException {
+		if (this.is == null) {
+			throw new IOException();
+		}
+		try {
+			parser.setInput(new InputStreamReader(this.is));
+		} catch (XmlPullParserException e) {
+			throw new IOException("error resetting parser");
+		}
+	}
+
+	public Tag readTag() throws XmlPullParserException, IOException {
+		if (wakeLock.isHeld()) {
+			try {
+				wakeLock.release();
+			} catch (RuntimeException re) {
+			}
+		}
+		try {
+			while (this.is != null
+					&& parser.next() != XmlPullParser.END_DOCUMENT) {
+				wakeLock.acquire();
+				if (parser.getEventType() == XmlPullParser.START_TAG) {
+					Tag tag = Tag.start(parser.getName());
+					for (int i = 0; i < parser.getAttributeCount(); ++i) {
+						tag.setAttribute(parser.getAttributeName(i),
+								parser.getAttributeValue(i));
+					}
+					String xmlns = parser.getNamespace();
+					if (xmlns != null) {
+						tag.setAttribute("xmlns", xmlns);
+					}
+					return tag;
+				} else if (parser.getEventType() == XmlPullParser.END_TAG) {
+					Tag tag = Tag.end(parser.getName());
+					return tag;
+				} else if (parser.getEventType() == XmlPullParser.TEXT) {
+					Tag tag = Tag.no(parser.getText());
+					return tag;
+				}
+			}
+			if (wakeLock.isHeld()) {
+				try {
+					wakeLock.release();
+				} catch (RuntimeException re) {
+				}
+			}
+		} catch (ArrayIndexOutOfBoundsException e) {
+			throw new IOException(
+					"xml parser mishandled ArrayIndexOufOfBounds", e);
+		} catch (StringIndexOutOfBoundsException e) {
+			throw new IOException(
+					"xml parser mishandled StringIndexOufOfBounds", e);
+		} catch (NullPointerException e) {
+			throw new IOException("xml parser mishandled NullPointerException",
+					e);
+		} catch (IndexOutOfBoundsException e) {
+			throw new IOException("xml parser mishandled IndexOutOfBound", e);
+		}
+		return null;
+	}
+
+	public Element readElement(Tag currentTag) throws XmlPullParserException,
+			IOException {
+		Element element = new Element(currentTag.getName());
+		element.setAttributes(currentTag.getAttributes());
+		Tag nextTag = this.readTag();
+		if (nextTag == null) {
+			throw new IOException("unterupted mid tag");
+		}
+		if (nextTag.isNo()) {
+			element.setContent(nextTag.getName());
+			nextTag = this.readTag();
+			if (nextTag == null) {
+				throw new IOException("unterupted mid tag");
+			}
+		}
+		while (!nextTag.isEnd(element.getName())) {
+			if (!nextTag.isNo()) {
+				Element child = this.readElement(nextTag);
+				element.addChild(child);
+			}
+			nextTag = this.readTag();
+			if (nextTag == null) {
+				throw new IOException("unterupted mid tag");
+			}
+		}
+		return element;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -0,0 +1,1130 @@
+package eu.siacs.conversations.xmpp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.xmlpull.v1.XmlPullParserException;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.util.SparseArray;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.DNSHelper;
+import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
+import eu.siacs.conversations.utils.zlib.ZLibInputStream;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Tag;
+import eu.siacs.conversations.xml.TagWriter;
+import eu.siacs.conversations.xml.XmlReader;
+import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
+
+public class XmppConnection implements Runnable {
+
+	protected Account account;
+
+	private WakeLock wakeLock;
+
+	private SecureRandom mRandom;
+
+	private Socket socket;
+	private XmlReader tagReader;
+	private TagWriter tagWriter;
+
+	private Features features = new Features(this);
+
+	private boolean shouldBind = true;
+	private boolean shouldAuthenticate = true;
+	private Element streamFeatures;
+	private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
+
+	private String streamId = null;
+	private int smVersion = 3;
+	private SparseArray<String> messageReceipts = new SparseArray<String>();
+
+	private boolean usingCompression = false;
+	private boolean usingEncryption = false;
+
+	private int stanzasReceived = 0;
+	private int stanzasSent = 0;
+
+	private long lastPaketReceived = 0;
+	private long lastPingSent = 0;
+	private long lastConnect = 0;
+	private long lastSessionStarted = 0;
+
+	private int attempt = 0;
+
+	private static final int PACKET_IQ = 0;
+	private static final int PACKET_MESSAGE = 1;
+	private static final int PACKET_PRESENCE = 2;
+
+	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
+	private OnPresencePacketReceived presenceListener = null;
+	private OnJinglePacketReceived jingleListener = null;
+	private OnIqPacketReceived unregisteredIqListener = null;
+	private OnMessagePacketReceived messageListener = null;
+	private OnStatusChanged statusListener = null;
+	private OnBindListener bindListener = null;
+	private OnMessageAcknowledged acknowledgedListener = null;
+	private MemorizingTrustManager mMemorizingTrustManager;
+	private final Context applicationContext;
+
+	public XmppConnection(Account account, XmppConnectionService service) {
+		this.mRandom = service.getRNG();
+		this.mMemorizingTrustManager = service.getMemorizingTrustManager();
+		this.account = account;
+		this.wakeLock = service.getPowerManager().newWakeLock(
+				PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
+		tagWriter = new TagWriter();
+		applicationContext = service.getApplicationContext();
+	}
+
+	protected void changeStatus(int nextStatus) {
+		if (account.getStatus() != nextStatus) {
+			if ((nextStatus == Account.STATUS_OFFLINE)
+					&& (account.getStatus() != Account.STATUS_CONNECTING)
+					&& (account.getStatus() != Account.STATUS_ONLINE)
+					&& (account.getStatus() != Account.STATUS_DISABLED)) {
+				return;
+			}
+			if (nextStatus == Account.STATUS_ONLINE) {
+				this.attempt = 0;
+			}
+			account.setStatus(nextStatus);
+			if (statusListener != null) {
+				statusListener.onStatusChanged(account);
+			}
+		}
+	}
+
+	protected void connect() {
+		Log.d(Config.LOGTAG, account.getJid() + ": connecting");
+		usingCompression = false;
+		usingEncryption = false;
+		lastConnect = SystemClock.elapsedRealtime();
+		lastPingSent = SystemClock.elapsedRealtime();
+		this.attempt++;
+		try {
+			shouldAuthenticate = shouldBind = !account
+					.isOptionSet(Account.OPTION_REGISTER);
+			tagReader = new XmlReader(wakeLock);
+			tagWriter = new TagWriter();
+			packetCallbacks.clear();
+			this.changeStatus(Account.STATUS_CONNECTING);
+			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
+			if ("timeout".equals(namePort.getString("error"))) {
+				Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
+				this.changeStatus(Account.STATUS_OFFLINE);
+				return;
+			}
+			String srvRecordServer = namePort.getString("name");
+			String srvIpServer = namePort.getString("ipv4");
+			int srvRecordPort = namePort.getInt("port");
+			if (srvRecordServer != null) {
+				if (srvIpServer != null) {
+					Log.d(Config.LOGTAG, account.getJid()
+							+ ": using values from dns " + srvRecordServer
+							+ "[" + srvIpServer + "]:" + srvRecordPort);
+					socket = new Socket(srvIpServer, srvRecordPort);
+				} else {
+					boolean socketError = true;
+					int srvIndex = 0;
+					while (socketError
+							&& namePort.containsKey("name" + srvIndex)) {
+						try {
+							srvRecordServer = namePort.getString("name"
+									+ srvIndex);
+							srvRecordPort = namePort.getInt("port" + srvIndex);
+							Log.d(Config.LOGTAG, account.getJid()
+									+ ": using values from dns "
+									+ srvRecordServer + ":" + srvRecordPort);
+							socket = new Socket(srvRecordServer, srvRecordPort);
+							socketError = false;
+						} catch (UnknownHostException e) {
+							srvIndex++;
+							if (!namePort.containsKey("name" + srvIndex)) {
+								throw e;
+							}
+						} catch (IOException e) {
+							srvIndex++;
+							if (!namePort.containsKey("name" + srvIndex)) {
+								throw e;
+							}
+						}
+					}
+				}
+			} else if (namePort.containsKey("error")
+					&& "nosrv".equals(namePort.getString("error", null))) {
+				socket = new Socket(account.getServer(), 5222);
+			} else {
+				Log.d(Config.LOGTAG, account.getJid()
+						+ ": timeout in DNS resolution");
+				changeStatus(Account.STATUS_OFFLINE);
+				return;
+			}
+			OutputStream out = socket.getOutputStream();
+			tagWriter.setOutputStream(out);
+			InputStream in = socket.getInputStream();
+			tagReader.setInputStream(in);
+			tagWriter.beginDocument();
+			sendStartStream();
+			Tag nextTag;
+			while ((nextTag = tagReader.readTag()) != null) {
+				if (nextTag.isStart("stream")) {
+					processStream(nextTag);
+					break;
+				} else {
+					Log.d(Config.LOGTAG,
+							"found unexpected tag: " + nextTag.getName());
+					return;
+				}
+			}
+			if (socket.isConnected()) {
+				socket.close();
+			}
+		} catch (UnknownHostException e) {
+			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
+			if (wakeLock.isHeld()) {
+				try {
+					wakeLock.release();
+				} catch (RuntimeException re) {
+				}
+			}
+			return;
+		} catch (IOException e) {
+			this.changeStatus(Account.STATUS_OFFLINE);
+			if (wakeLock.isHeld()) {
+				try {
+					wakeLock.release();
+				} catch (RuntimeException re) {
+				}
+			}
+			return;
+		} catch (NoSuchAlgorithmException e) {
+			this.changeStatus(Account.STATUS_OFFLINE);
+			Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
+			if (wakeLock.isHeld()) {
+				try {
+					wakeLock.release();
+				} catch (RuntimeException re) {
+				}
+			}
+			return;
+		} catch (XmlPullParserException e) {
+			this.changeStatus(Account.STATUS_OFFLINE);
+			Log.d(Config.LOGTAG, "xml exception " + e.getMessage());
+			if (wakeLock.isHeld()) {
+				try {
+					wakeLock.release();
+				} catch (RuntimeException re) {
+				}
+			}
+			return;
+		}
+
+	}
+
+	@Override
+	public void run() {
+		connect();
+	}
+
+	private void processStream(Tag currentTag) throws XmlPullParserException,
+			IOException, NoSuchAlgorithmException {
+		Tag nextTag = tagReader.readTag();
+		while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
+			if (nextTag.isStart("error")) {
+				processStreamError(nextTag);
+			} else if (nextTag.isStart("features")) {
+				processStreamFeatures(nextTag);
+			} else if (nextTag.isStart("proceed")) {
+				switchOverToTls(nextTag);
+			} else if (nextTag.isStart("compressed")) {
+				switchOverToZLib(nextTag);
+			} else if (nextTag.isStart("success")) {
+				Log.d(Config.LOGTAG, account.getJid() + ": logged in");
+				tagReader.readTag();
+				tagReader.reset();
+				sendStartStream();
+				processStream(tagReader.readTag());
+				break;
+			} else if (nextTag.isStart("failure")) {
+				tagReader.readElement(nextTag);
+				changeStatus(Account.STATUS_UNAUTHORIZED);
+			} else if (nextTag.isStart("challenge")) {
+				String challange = tagReader.readElement(nextTag).getContent();
+				Element response = new Element("response");
+				response.setAttribute("xmlns",
+						"urn:ietf:params:xml:ns:xmpp-sasl");
+				response.setContent(CryptoHelper.saslDigestMd5(account,
+						challange, mRandom));
+				tagWriter.writeElement(response);
+			} else if (nextTag.isStart("enabled")) {
+				Element enabled = tagReader.readElement(nextTag);
+				if ("true".equals(enabled.getAttribute("resume"))) {
+					this.streamId = enabled.getAttribute("id");
+					Log.d(Config.LOGTAG, account.getJid()
+							+ ": stream managment(" + smVersion
+							+ ") enabled (resumable)");
+				} else {
+					Log.d(Config.LOGTAG, account.getJid()
+							+ ": stream managment(" + smVersion + ") enabled");
+				}
+				this.lastSessionStarted = SystemClock.elapsedRealtime();
+				this.stanzasReceived = 0;
+				RequestPacket r = new RequestPacket(smVersion);
+				tagWriter.writeStanzaAsync(r);
+			} else if (nextTag.isStart("resumed")) {
+				lastPaketReceived = SystemClock.elapsedRealtime();
+				Element resumed = tagReader.readElement(nextTag);
+				String h = resumed.getAttribute("h");
+				try {
+					int serverCount = Integer.parseInt(h);
+					if (serverCount != stanzasSent) {
+						Log.d(Config.LOGTAG, account.getJid()
+								+ ": session resumed with lost packages");
+						stanzasSent = serverCount;
+					} else {
+						Log.d(Config.LOGTAG, account.getJid()
+								+ ": session resumed");
+					}
+					if (acknowledgedListener != null) {
+						for (int i = 0; i < messageReceipts.size(); ++i) {
+							if (serverCount >= messageReceipts.keyAt(i)) {
+								acknowledgedListener.onMessageAcknowledged(
+										account, messageReceipts.valueAt(i));
+							}
+						}
+					}
+					messageReceipts.clear();
+				} catch (NumberFormatException e) {
+
+				}
+				sendInitialPing();
+
+			} else if (nextTag.isStart("r")) {
+				tagReader.readElement(nextTag);
+				AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
+				tagWriter.writeStanzaAsync(ack);
+			} else if (nextTag.isStart("a")) {
+				Element ack = tagReader.readElement(nextTag);
+				lastPaketReceived = SystemClock.elapsedRealtime();
+				int serverSequence = Integer.parseInt(ack.getAttribute("h"));
+				String msgId = this.messageReceipts.get(serverSequence);
+				if (msgId != null) {
+					if (this.acknowledgedListener != null) {
+						this.acknowledgedListener.onMessageAcknowledged(
+								account, msgId);
+					}
+					this.messageReceipts.remove(serverSequence);
+				}
+			} else if (nextTag.isStart("failed")) {
+				tagReader.readElement(nextTag);
+				Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
+				streamId = null;
+				if (account.getStatus() != Account.STATUS_ONLINE) {
+					sendBindRequest();
+				}
+			} else if (nextTag.isStart("iq")) {
+				processIq(nextTag);
+			} else if (nextTag.isStart("message")) {
+				processMessage(nextTag);
+			} else if (nextTag.isStart("presence")) {
+				processPresence(nextTag);
+			}
+			nextTag = tagReader.readTag();
+		}
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			account.setStatus(Account.STATUS_OFFLINE);
+			if (statusListener != null) {
+				statusListener.onStatusChanged(account);
+			}
+		}
+	}
+
+	private void sendInitialPing() {
+		Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping");
+		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+		iq.setFrom(account.getFullJid());
+		iq.addChild("ping", "urn:xmpp:ping");
+		this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				Log.d(Config.LOGTAG, account.getJid()
+						+ ": online with resource " + account.getResource());
+				changeStatus(Account.STATUS_ONLINE);
+			}
+		});
+	}
+
+	private Element processPacket(Tag currentTag, int packetType)
+			throws XmlPullParserException, IOException {
+		Element element;
+		switch (packetType) {
+		case PACKET_IQ:
+			element = new IqPacket();
+			break;
+		case PACKET_MESSAGE:
+			element = new MessagePacket();
+			break;
+		case PACKET_PRESENCE:
+			element = new PresencePacket();
+			break;
+		default:
+			return null;
+		}
+		element.setAttributes(currentTag.getAttributes());
+		Tag nextTag = tagReader.readTag();
+		if (nextTag == null) {
+			throw new IOException("interrupted mid tag");
+		}
+		while (!nextTag.isEnd(element.getName())) {
+			if (!nextTag.isNo()) {
+				Element child = tagReader.readElement(nextTag);
+				String type = currentTag.getAttribute("type");
+				if (packetType == PACKET_IQ
+						&& "jingle".equals(child.getName())
+						&& ("set".equalsIgnoreCase(type) || "get"
+								.equalsIgnoreCase(type))) {
+					element = new JinglePacket();
+					element.setAttributes(currentTag.getAttributes());
+				}
+				element.addChild(child);
+			}
+			nextTag = tagReader.readTag();
+			if (nextTag == null) {
+				throw new IOException("interrupted mid tag");
+			}
+		}
+		++stanzasReceived;
+		lastPaketReceived = SystemClock.elapsedRealtime();
+		return element;
+	}
+
+	private void processIq(Tag currentTag) throws XmlPullParserException,
+			IOException {
+		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
+
+		if (packet.getId() == null) {
+			return; // an iq packet without id is definitely invalid
+		}
+
+		if (packet instanceof JinglePacket) {
+			if (this.jingleListener != null) {
+				this.jingleListener.onJinglePacketReceived(account,
+						(JinglePacket) packet);
+			}
+		} else {
+			if (packetCallbacks.containsKey(packet.getId())) {
+				if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
+					((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
+							.onIqPacketReceived(account, packet);
+				}
+
+				packetCallbacks.remove(packet.getId());
+			} else if ((packet.getType() == IqPacket.TYPE_GET || packet
+					.getType() == IqPacket.TYPE_SET)
+					&& this.unregisteredIqListener != null) {
+				this.unregisteredIqListener.onIqPacketReceived(account, packet);
+			}
+		}
+	}
+
+	private void processMessage(Tag currentTag) throws XmlPullParserException,
+			IOException {
+		MessagePacket packet = (MessagePacket) processPacket(currentTag,
+				PACKET_MESSAGE);
+		String id = packet.getAttribute("id");
+		if ((id != null) && (packetCallbacks.containsKey(id))) {
+			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
+				((OnMessagePacketReceived) packetCallbacks.get(id))
+						.onMessagePacketReceived(account, packet);
+			}
+			packetCallbacks.remove(id);
+		} else if (this.messageListener != null) {
+			this.messageListener.onMessagePacketReceived(account, packet);
+		}
+	}
+
+	private void processPresence(Tag currentTag) throws XmlPullParserException,
+			IOException {
+		PresencePacket packet = (PresencePacket) processPacket(currentTag,
+				PACKET_PRESENCE);
+		String id = packet.getAttribute("id");
+		if ((id != null) && (packetCallbacks.containsKey(id))) {
+			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
+				((OnPresencePacketReceived) packetCallbacks.get(id))
+						.onPresencePacketReceived(account, packet);
+			}
+			packetCallbacks.remove(id);
+		} else if (this.presenceListener != null) {
+			this.presenceListener.onPresencePacketReceived(account, packet);
+		}
+	}
+
+	private void sendCompressionZlib() throws IOException {
+		Element compress = new Element("compress");
+		compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
+		compress.addChild("method").setContent("zlib");
+		tagWriter.writeElement(compress);
+	}
+
+	private void switchOverToZLib(Tag currentTag)
+			throws XmlPullParserException, IOException,
+			NoSuchAlgorithmException {
+		tagReader.readTag(); // read tag close
+		tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
+				.getOutputStream()));
+		tagReader
+				.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
+
+		sendStartStream();
+		Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
+		usingCompression = true;
+		processStream(tagReader.readTag());
+	}
+
+	private void sendStartTLS() throws IOException {
+		Tag startTLS = Tag.empty("starttls");
+		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
+		tagWriter.writeTag(startTLS);
+	}
+
+	private SharedPreferences getPreferences() {
+		return PreferenceManager
+				.getDefaultSharedPreferences(applicationContext);
+	}
+
+	private boolean enableLegacySSL() {
+		return getPreferences().getBoolean("enable_legacy_ssl", false);
+	}
+
+	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
+			IOException {
+		tagReader.readTag();
+		try {
+			SSLContext sc = SSLContext.getInstance("TLS");
+			sc.init(null,
+					new X509TrustManager[] { this.mMemorizingTrustManager },
+					mRandom);
+			SSLSocketFactory factory = sc.getSocketFactory();
+
+			HostnameVerifier verifier = this.mMemorizingTrustManager
+					.wrapHostnameVerifier(new StrictHostnameVerifier());
+			SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
+					socket.getInetAddress().getHostAddress(), socket.getPort(),
+					true);
+
+			// Support all protocols except legacy SSL.
+			// The min SDK version prevents us having to worry about SSLv2. In
+			// future, this may be
+			// true of SSLv3 as well.
+			final String[] supportProtocols;
+			if (enableLegacySSL()) {
+				supportProtocols = sslSocket.getSupportedProtocols();
+			} else {
+				final List<String> supportedProtocols = new LinkedList<String>(
+						Arrays.asList(sslSocket.getSupportedProtocols()));
+				supportedProtocols.remove("SSLv3");
+				supportProtocols = new String[supportedProtocols.size()];
+				supportedProtocols.toArray(supportProtocols);
+			}
+			sslSocket.setEnabledProtocols(supportProtocols);
+
+			if (verifier != null
+					&& !verifier.verify(account.getServer(),
+							sslSocket.getSession())) {
+				Log.d(Config.LOGTAG, account.getJid()
+						+ ": host mismatch in TLS connection");
+				sslSocket.close();
+				throw new IOException();
+			}
+			tagReader.setInputStream(sslSocket.getInputStream());
+			tagWriter.setOutputStream(sslSocket.getOutputStream());
+			sendStartStream();
+			Log.d(Config.LOGTAG, account.getJid()
+					+ ": TLS connection established");
+			usingEncryption = true;
+			processStream(tagReader.readTag());
+			sslSocket.close();
+		} catch (NoSuchAlgorithmException e1) {
+			e1.printStackTrace();
+		} catch (KeyManagementException e) {
+			e.printStackTrace();
+		}
+	}
+
+	private void sendSaslAuthPlain() throws IOException {
+		String saslString = CryptoHelper.saslPlain(account.getUsername(),
+				account.getPassword());
+		Element auth = new Element("auth");
+		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+		auth.setAttribute("mechanism", "PLAIN");
+		auth.setContent(saslString);
+		tagWriter.writeElement(auth);
+	}
+
+	private void sendSaslAuthDigestMd5() throws IOException {
+		Element auth = new Element("auth");
+		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+		auth.setAttribute("mechanism", "DIGEST-MD5");
+		tagWriter.writeElement(auth);
+	}
+
+	private void processStreamFeatures(Tag currentTag)
+			throws XmlPullParserException, IOException {
+		this.streamFeatures = tagReader.readElement(currentTag);
+		if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
+			sendStartTLS();
+		} else if (compressionAvailable()) {
+			sendCompressionZlib();
+		} else if (this.streamFeatures.hasChild("register")
+				&& account.isOptionSet(Account.OPTION_REGISTER)
+				&& usingEncryption) {
+			sendRegistryRequest();
+		} else if (!this.streamFeatures.hasChild("register")
+				&& account.isOptionSet(Account.OPTION_REGISTER)) {
+			changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
+			disconnect(true);
+		} else if (this.streamFeatures.hasChild("mechanisms")
+				&& shouldAuthenticate && usingEncryption) {
+			List<String> mechanisms = extractMechanisms(streamFeatures
+					.findChild("mechanisms"));
+			if (mechanisms.contains("PLAIN")) {
+				sendSaslAuthPlain();
+			} else if (mechanisms.contains("DIGEST-MD5")) {
+				sendSaslAuthDigestMd5();
+			}
+		} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
+				+ smVersion)
+				&& streamId != null) {
+			ResumePacket resume = new ResumePacket(this.streamId,
+					stanzasReceived, smVersion);
+			this.tagWriter.writeStanzaAsync(resume);
+		} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
+			sendBindRequest();
+		} else {
+			Log.d(Config.LOGTAG, account.getJid()
+					+ ": incompatible server. disconnecting");
+			disconnect(true);
+		}
+	}
+
+	private boolean compressionAvailable() {
+		if (!this.streamFeatures.hasChild("compression",
+				"http://jabber.org/features/compress"))
+			return false;
+		if (!ZLibOutputStream.SUPPORTED)
+			return false;
+		if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
+			return false;
+
+		Element compression = this.streamFeatures.findChild("compression",
+				"http://jabber.org/features/compress");
+		for (Element child : compression.getChildren()) {
+			if (!"method".equals(child.getName()))
+				continue;
+
+			if ("zlib".equalsIgnoreCase(child.getContent())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private List<String> extractMechanisms(Element stream) {
+		ArrayList<String> mechanisms = new ArrayList<String>(stream
+				.getChildren().size());
+		for (Element child : stream.getChildren()) {
+			mechanisms.add(child.getContent());
+		}
+		return mechanisms;
+	}
+
+	private void sendRegistryRequest() {
+		IqPacket register = new IqPacket(IqPacket.TYPE_GET);
+		register.query("jabber:iq:register");
+		register.setTo(account.getServer());
+		sendIqPacket(register, new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				Element instructions = packet.query().findChild("instructions");
+				if (packet.query().hasChild("username")
+						&& (packet.query().hasChild("password"))) {
+					IqPacket register = new IqPacket(IqPacket.TYPE_SET);
+					Element username = new Element("username")
+							.setContent(account.getUsername());
+					Element password = new Element("password")
+							.setContent(account.getPassword());
+					register.query("jabber:iq:register").addChild(username);
+					register.query().addChild(password);
+					sendIqPacket(register, new OnIqPacketReceived() {
+
+						@Override
+						public void onIqPacketReceived(Account account,
+								IqPacket packet) {
+							if (packet.getType() == IqPacket.TYPE_RESULT) {
+								account.setOption(Account.OPTION_REGISTER,
+										false);
+								changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
+							} else if (packet.hasChild("error")
+									&& (packet.findChild("error")
+											.hasChild("conflict"))) {
+								changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
+							} else {
+								changeStatus(Account.STATUS_REGISTRATION_FAILED);
+								Log.d(Config.LOGTAG, packet.toString());
+							}
+							disconnect(true);
+						}
+					});
+				} else {
+					changeStatus(Account.STATUS_REGISTRATION_FAILED);
+					disconnect(true);
+					Log.d(Config.LOGTAG, account.getJid()
+							+ ": could not register. instructions are"
+							+ instructions.getContent());
+				}
+			}
+		});
+	}
+
+	private void sendBindRequest() throws IOException {
+		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
+				.addChild("resource").setContent(account.getResource());
+		this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				Element bind = packet.findChild("bind");
+				if (bind != null) {
+					Element jid = bind.findChild("jid");
+					if (jid != null && jid.getContent() != null) {
+						account.setResource(jid.getContent().split("/", 2)[1]);
+						if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
+							smVersion = 3;
+							EnablePacket enable = new EnablePacket(smVersion);
+							tagWriter.writeStanzaAsync(enable);
+							stanzasSent = 0;
+							messageReceipts.clear();
+						} else if (streamFeatures.hasChild("sm",
+								"urn:xmpp:sm:2")) {
+							smVersion = 2;
+							EnablePacket enable = new EnablePacket(smVersion);
+							tagWriter.writeStanzaAsync(enable);
+							stanzasSent = 0;
+							messageReceipts.clear();
+						}
+						sendServiceDiscoveryInfo(account.getServer());
+						sendServiceDiscoveryItems(account.getServer());
+						if (bindListener != null) {
+							bindListener.onBind(account);
+						}
+						sendInitialPing();
+					} else {
+						disconnect(true);
+					}
+				} else {
+					disconnect(true);
+				}
+			}
+		});
+		if (this.streamFeatures.hasChild("session")) {
+			Log.d(Config.LOGTAG, account.getJid()
+					+ ": sending deprecated session");
+			IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
+			startSession.addChild("session",
+					"urn:ietf:params:xml:ns:xmpp-session");
+			this.sendUnboundIqPacket(startSession, null);
+		}
+	}
+
+	private void sendServiceDiscoveryInfo(final String server) {
+		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+		iq.setTo(server);
+		iq.query("http://jabber.org/protocol/disco#info");
+		this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				List<Element> elements = packet.query().getChildren();
+				List<String> features = new ArrayList<String>();
+				for (int i = 0; i < elements.size(); ++i) {
+					if (elements.get(i).getName().equals("feature")) {
+						features.add(elements.get(i).getAttribute("var"));
+					}
+				}
+				disco.put(server, features);
+
+				if (account.getServer().equals(server)) {
+					enableAdvancedStreamFeatures();
+				}
+			}
+		});
+	}
+
+	private void enableAdvancedStreamFeatures() {
+		if (getFeatures().carbons()) {
+			sendEnableCarbons();
+		}
+	}
+
+	private void sendServiceDiscoveryItems(final String server) {
+		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+		iq.setTo(server);
+		iq.query("http://jabber.org/protocol/disco#items");
+		this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				List<Element> elements = packet.query().getChildren();
+				for (int i = 0; i < elements.size(); ++i) {
+					if (elements.get(i).getName().equals("item")) {
+						String jid = elements.get(i).getAttribute("jid");
+						sendServiceDiscoveryInfo(jid);
+					}
+				}
+			}
+		});
+	}
+
+	private void sendEnableCarbons() {
+		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+		iq.addChild("enable", "urn:xmpp:carbons:2");
+		this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				if (!packet.hasChild("error")) {
+					Log.d(Config.LOGTAG, account.getJid()
+							+ ": successfully enabled carbons");
+				} else {
+					Log.d(Config.LOGTAG, account.getJid()
+							+ ": error enableing carbons " + packet.toString());
+				}
+			}
+		});
+	}
+
+	private void processStreamError(Tag currentTag)
+			throws XmlPullParserException, IOException {
+		Element streamError = tagReader.readElement(currentTag);
+		if (streamError != null && streamError.hasChild("conflict")) {
+			String resource = account.getResource().split("\\.")[0];
+			account.setResource(resource + "." + nextRandomId());
+			Log.d(Config.LOGTAG,
+					account.getJid() + ": switching resource due to conflict ("
+							+ account.getResource() + ")");
+		}
+	}
+
+	private void sendStartStream() throws IOException {
+		Tag stream = Tag.start("stream:stream");
+		stream.setAttribute("from", account.getJid());
+		stream.setAttribute("to", account.getServer());
+		stream.setAttribute("version", "1.0");
+		stream.setAttribute("xml:lang", "en");
+		stream.setAttribute("xmlns", "jabber:client");
+		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
+		tagWriter.writeTag(stream);
+	}
+
+	private String nextRandomId() {
+		return new BigInteger(50, mRandom).toString(32);
+	}
+
+	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
+		if (packet.getId() == null) {
+			String id = nextRandomId();
+			packet.setAttribute("id", id);
+		}
+		packet.setFrom(account.getFullJid());
+		this.sendPacket(packet, callback);
+	}
+
+	public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
+		if (packet.getId() == null) {
+			String id = nextRandomId();
+			packet.setAttribute("id", id);
+		}
+		this.sendPacket(packet, callback);
+	}
+
+	public void sendMessagePacket(MessagePacket packet) {
+		this.sendPacket(packet, null);
+	}
+
+	public void sendPresencePacket(PresencePacket packet) {
+		this.sendPacket(packet, null);
+	}
+
+	private synchronized void sendPacket(final AbstractStanza packet,
+			PacketReceived callback) {
+		if (packet.getName().equals("iq") || packet.getName().equals("message")
+				|| packet.getName().equals("presence")) {
+			++stanzasSent;
+		}
+		tagWriter.writeStanzaAsync(packet);
+		if (packet instanceof MessagePacket && packet.getId() != null
+				&& this.streamId != null) {
+			Log.d(Config.LOGTAG, "request delivery report for stanza "
+					+ stanzasSent);
+			this.messageReceipts.put(stanzasSent, packet.getId());
+			tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
+		}
+		if (callback != null) {
+			if (packet.getId() == null) {
+				packet.setId(nextRandomId());
+			}
+			packetCallbacks.put(packet.getId(), callback);
+		}
+	}
+
+	public void sendPing() {
+		if (streamFeatures.hasChild("sm")) {
+			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+		} else {
+			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+			iq.setFrom(account.getFullJid());
+			iq.addChild("ping", "urn:xmpp:ping");
+			this.sendIqPacket(iq, null);
+		}
+		this.lastPingSent = SystemClock.elapsedRealtime();
+	}
+
+	public void setOnMessagePacketReceivedListener(
+			OnMessagePacketReceived listener) {
+		this.messageListener = listener;
+	}
+
+	public void setOnUnregisteredIqPacketReceivedListener(
+			OnIqPacketReceived listener) {
+		this.unregisteredIqListener = listener;
+	}
+
+	public void setOnPresencePacketReceivedListener(
+			OnPresencePacketReceived listener) {
+		this.presenceListener = listener;
+	}
+
+	public void setOnJinglePacketReceivedListener(
+			OnJinglePacketReceived listener) {
+		this.jingleListener = listener;
+	}
+
+	public void setOnStatusChangedListener(OnStatusChanged listener) {
+		this.statusListener = listener;
+	}
+
+	public void setOnBindListener(OnBindListener listener) {
+		this.bindListener = listener;
+	}
+
+	public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
+		this.acknowledgedListener = listener;
+	}
+
+	public void disconnect(boolean force) {
+		Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
+		try {
+			if (force) {
+				socket.close();
+				return;
+			}
+			new Thread(new Runnable() {
+
+				@Override
+				public void run() {
+					if (tagWriter.isActive()) {
+						tagWriter.finish();
+						try {
+							while (!tagWriter.finished()) {
+								Log.d(Config.LOGTAG, "not yet finished");
+								Thread.sleep(100);
+							}
+							tagWriter.writeTag(Tag.end("stream:stream"));
+							socket.close();
+						} catch (IOException e) {
+							Log.d(Config.LOGTAG,
+									"io exception during disconnect");
+						} catch (InterruptedException e) {
+							Log.d(Config.LOGTAG, "interrupted");
+						}
+					}
+				}
+			}).start();
+		} catch (IOException e) {
+			Log.d(Config.LOGTAG, "io exception during disconnect");
+		}
+	}
+
+	public List<String> findDiscoItemsByFeature(String feature) {
+		List<String> items = new ArrayList<String>();
+		for (Entry<String, List<String>> cursor : disco.entrySet()) {
+			if (cursor.getValue().contains(feature)) {
+				items.add(cursor.getKey());
+			}
+		}
+		return items;
+	}
+
+	public String findDiscoItemByFeature(String feature) {
+		List<String> items = findDiscoItemsByFeature(feature);
+		if (items.size() >= 1) {
+			return items.get(0);
+		}
+		return null;
+	}
+
+	public void r() {
+		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+	}
+
+	public String getMucServer() {
+		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
+	}
+
+	public int getTimeToNextAttempt() {
+		int interval = (int) (25 * Math.pow(1.5, attempt));
+		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
+		return interval - secondsSinceLast;
+	}
+
+	public int getAttempt() {
+		return this.attempt;
+	}
+
+	public Features getFeatures() {
+		return this.features;
+	}
+
+	public class Features {
+		XmppConnection connection;
+
+		public Features(XmppConnection connection) {
+			this.connection = connection;
+		}
+
+		private boolean hasDiscoFeature(String server, String feature) {
+			if (!connection.disco.containsKey(server)) {
+				return false;
+			}
+			return connection.disco.get(server).contains(feature);
+		}
+
+		public boolean carbons() {
+			return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
+		}
+
+		public boolean sm() {
+			return streamId != null;
+		}
+
+		public boolean csi() {
+			if (connection.streamFeatures == null) {
+				return false;
+			} else {
+				return connection.streamFeatures.hasChild("csi",
+						"urn:xmpp:csi:0");
+			}
+		}
+
+		public boolean pubsub() {
+			return hasDiscoFeature(account.getServer(),
+					"http://jabber.org/protocol/pubsub#publish");
+		}
+
+		public boolean rosterVersioning() {
+			if (connection.streamFeatures == null) {
+				return false;
+			} else {
+				return connection.streamFeatures.hasChild("ver");
+			}
+		}
+
+		public boolean streamhost() {
+			return connection
+					.findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
+		}
+
+		public boolean compression() {
+			return connection.usingCompression;
+		}
+	}
+
+	public long getLastSessionEstablished() {
+		long diff;
+		if (this.lastSessionStarted == 0) {
+			diff = SystemClock.elapsedRealtime() - this.lastConnect;
+		} else {
+			diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
+		}
+		return System.currentTimeMillis() - diff;
+	}
+
+	public long getLastConnect() {
+		return this.lastConnect;
+	}
+
+	public long getLastPingSent() {
+		return this.lastPingSent;
+	}
+
+	public long getLastPacketReceived() {
+		return this.lastPaketReceived;
+	}
+
+	public void sendActive() {
+		this.sendPacket(new ActivePacket(), null);
+	}
+
+	public void sendInactive() {
+		this.sendPacket(new InactivePacket(), null);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java 🔗

@@ -0,0 +1,143 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+public class JingleCandidate {
+
+	public static int TYPE_UNKNOWN;
+	public static int TYPE_DIRECT = 0;
+	public static int TYPE_PROXY = 1;
+
+	private boolean ours;
+	private boolean usedByCounterpart = false;
+	private String cid;
+	private String host;
+	private int port;
+	private int type;
+	private String jid;
+	private int priority;
+
+	public JingleCandidate(String cid, boolean ours) {
+		this.ours = ours;
+		this.cid = cid;
+	}
+
+	public String getCid() {
+		return cid;
+	}
+
+	public void setHost(String host) {
+		this.host = host;
+	}
+
+	public String getHost() {
+		return this.host;
+	}
+
+	public void setJid(String jid) {
+		this.jid = jid;
+	}
+
+	public String getJid() {
+		return this.jid;
+	}
+
+	public void setPort(int port) {
+		this.port = port;
+	}
+
+	public int getPort() {
+		return this.port;
+	}
+
+	public void setType(int type) {
+		this.type = type;
+	}
+
+	public void setType(String type) {
+		if ("proxy".equals(type)) {
+			this.type = TYPE_PROXY;
+		} else if ("direct".equals(type)) {
+			this.type = TYPE_DIRECT;
+		} else {
+			this.type = TYPE_UNKNOWN;
+		}
+	}
+
+	public void setPriority(int i) {
+		this.priority = i;
+	}
+
+	public int getPriority() {
+		return this.priority;
+	}
+
+	public boolean equals(JingleCandidate other) {
+		return this.getCid().equals(other.getCid());
+	}
+
+	public boolean equalValues(JingleCandidate other) {
+		return other.getHost().equals(this.getHost())
+				&& (other.getPort() == this.getPort());
+	}
+
+	public boolean isOurs() {
+		return ours;
+	}
+
+	public int getType() {
+		return this.type;
+	}
+
+	public static List<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() + ")";
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java 🔗

@@ -0,0 +1,910 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnection implements Downloadable {
+
+	private final String[] extensions = { "webp", "jpeg", "jpg", "png" };
+	private final String[] cryptoExtensions = { "pgp", "gpg", "otr" };
+
+	private JingleConnectionManager mJingleConnectionManager;
+	private XmppConnectionService mXmppConnectionService;
+
+	protected static final int JINGLE_STATUS_INITIATED = 0;
+	protected static final int JINGLE_STATUS_ACCEPTED = 1;
+	protected static final int JINGLE_STATUS_TERMINATED = 2;
+	protected static final int JINGLE_STATUS_CANCELED = 3;
+	protected static final int JINGLE_STATUS_FINISHED = 4;
+	protected static final int JINGLE_STATUS_TRANSMITTING = 5;
+	protected static final int JINGLE_STATUS_FAILED = 99;
+
+	private int ibbBlockSize = 4096;
+
+	private int mJingleStatus = -1;
+	private int mStatus = -1;
+	private Message message;
+	private String sessionId;
+	private Account account;
+	private String initiator;
+	private String responder;
+	private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
+	private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<String, JingleSocks5Transport>();
+
+	private String transportId;
+	private Element fileOffer;
+	private DownloadableFile file = null;
+
+	private String contentName;
+	private String contentCreator;
+
+	private boolean receivedCandidate = false;
+	private boolean sentCandidate = false;
+
+	private boolean acceptedAutomatically = false;
+
+	private JingleTransport transport = null;
+
+	private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
+
+		@Override
+		public void onIqPacketReceived(Account account, IqPacket packet) {
+			if (packet.getType() == IqPacket.TYPE_ERROR) {
+				if (initiator.equals(account.getFullJid())) {
+					mXmppConnectionService.markMessage(message,
+							Message.STATUS_SEND_FAILED);
+				}
+				mJingleStatus = JINGLE_STATUS_FAILED;
+			}
+		}
+	};
+
+	final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
+
+		@Override
+		public void onFileTransmitted(DownloadableFile file) {
+			if (responder.equals(account.getFullJid())) {
+				sendSuccess();
+				if (acceptedAutomatically) {
+					message.markUnread();
+					JingleConnection.this.mXmppConnectionService
+							.getNotificationService().push(message);
+				}
+				BitmapFactory.Options options = new BitmapFactory.Options();
+				options.inJustDecodeBounds = true;
+				BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+				int imageHeight = options.outHeight;
+				int imageWidth = options.outWidth;
+				message.setBody(Long.toString(file.getSize()) + ','
+						+ imageWidth + ',' + imageHeight);
+				mXmppConnectionService.databaseBackend.createMessage(message);
+				mXmppConnectionService.markMessage(message,
+						Message.STATUS_RECEIVED);
+			}
+			Log.d(Config.LOGTAG,
+					"sucessfully transmitted file:" + file.getAbsolutePath());
+			if (message.getEncryption() != Message.ENCRYPTION_PGP) {
+				Intent intent = new Intent(
+						Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+				intent.setData(Uri.fromFile(file));
+				mXmppConnectionService.sendBroadcast(intent);
+			}
+		}
+
+		@Override
+		public void onFileTransferAborted() {
+			JingleConnection.this.sendCancel();
+			JingleConnection.this.cancel();
+		}
+	};
+
+	private OnProxyActivated onProxyActivated = new OnProxyActivated() {
+
+		@Override
+		public void success() {
+			if (initiator.equals(account.getFullJid())) {
+				Log.d(Config.LOGTAG, "we were initiating. sending file");
+				transport.send(file, onFileTransmissionSatusChanged);
+			} else {
+				transport.receive(file, onFileTransmissionSatusChanged);
+				Log.d(Config.LOGTAG, "we were responding. receiving file");
+			}
+		}
+
+		@Override
+		public void failed() {
+			Log.d(Config.LOGTAG, "proxy activation failed");
+		}
+	};
+
+	public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
+		this.mJingleConnectionManager = mJingleConnectionManager;
+		this.mXmppConnectionService = mJingleConnectionManager
+				.getXmppConnectionService();
+	}
+
+	public String getSessionId() {
+		return this.sessionId;
+	}
+
+	public Account getAccount() {
+		return this.account;
+	}
+
+	public String getCounterPart() {
+		return this.message.getCounterpart();
+	}
+
+	public void deliverPacket(JinglePacket packet) {
+		boolean returnResult = true;
+		if (packet.isAction("session-terminate")) {
+			Reason reason = packet.getReason();
+			if (reason != null) {
+				if (reason.hasChild("cancel")) {
+					this.cancel();
+				} else if (reason.hasChild("success")) {
+					this.receiveSuccess();
+				} else {
+					this.cancel();
+				}
+			} else {
+				this.cancel();
+			}
+		} else if (packet.isAction("session-accept")) {
+			returnResult = receiveAccept(packet);
+		} else if (packet.isAction("transport-info")) {
+			returnResult = receiveTransportInfo(packet);
+		} else if (packet.isAction("transport-replace")) {
+			if (packet.getJingleContent().hasIbbTransport()) {
+				returnResult = this.receiveFallbackToIbb(packet);
+			} else {
+				returnResult = false;
+				Log.d(Config.LOGTAG, "trying to fallback to something unknown"
+						+ packet.toString());
+			}
+		} else if (packet.isAction("transport-accept")) {
+			returnResult = this.receiveTransportAccept(packet);
+		} else {
+			Log.d(Config.LOGTAG, "packet arrived in connection. action was "
+					+ packet.getAction());
+			returnResult = false;
+		}
+		IqPacket response;
+		if (returnResult) {
+			response = packet.generateRespone(IqPacket.TYPE_RESULT);
+
+		} else {
+			response = packet.generateRespone(IqPacket.TYPE_ERROR);
+		}
+		account.getXmppConnection().sendIqPacket(response, null);
+	}
+
+	public void init(Message message) {
+		this.contentCreator = "initiator";
+		this.contentName = this.mJingleConnectionManager.nextRandomId();
+		this.message = message;
+		this.account = message.getConversation().getAccount();
+		this.initiator = this.account.getFullJid();
+		this.responder = this.message.getCounterpart();
+		this.sessionId = this.mJingleConnectionManager.nextRandomId();
+		if (this.candidates.size() > 0) {
+			this.sendInitRequest();
+		} else {
+			this.mJingleConnectionManager.getPrimaryCandidate(account,
+					new OnPrimaryCandidateFound() {
+
+						@Override
+						public void onPrimaryCandidateFound(boolean success,
+								final JingleCandidate candidate) {
+							if (success) {
+								final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+										JingleConnection.this, candidate);
+								connections.put(candidate.getCid(),
+										socksConnection);
+								socksConnection
+										.connect(new OnTransportConnected() {
+
+											@Override
+											public void failed() {
+												Log.d(Config.LOGTAG,
+														"connection to our own primary candidete failed");
+												sendInitRequest();
+											}
+
+											@Override
+											public void established() {
+												Log.d(Config.LOGTAG,
+														"succesfully connected to our own primary candidate");
+												mergeCandidate(candidate);
+												sendInitRequest();
+											}
+										});
+								mergeCandidate(candidate);
+							} else {
+								Log.d(Config.LOGTAG,
+										"no primary candidate of our own was found");
+								sendInitRequest();
+							}
+						}
+					});
+		}
+
+	}
+
+	public void init(Account account, JinglePacket packet) {
+		this.mJingleStatus = JINGLE_STATUS_INITIATED;
+		Conversation conversation = this.mXmppConnectionService
+				.findOrCreateConversation(account,
+						packet.getFrom().split("/", 2)[0], false);
+		this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+		this.message.setStatus(Message.STATUS_RECEIVED);
+		this.message.setType(Message.TYPE_IMAGE);
+		this.mStatus = Downloadable.STATUS_OFFER;
+		this.message.setDownloadable(this);
+		String[] fromParts = packet.getFrom().split("/", 2);
+		this.message.setPresence(fromParts[1]);
+		this.account = account;
+		this.initiator = packet.getFrom();
+		this.responder = this.account.getFullJid();
+		this.sessionId = packet.getSessionId();
+		Content content = packet.getJingleContent();
+		this.contentCreator = content.getAttribute("creator");
+		this.contentName = content.getAttribute("name");
+		this.transportId = content.getTransportId();
+		this.mergeCandidates(JingleCandidate.parse(content.socks5transport()
+				.getChildren()));
+		this.fileOffer = packet.getJingleContent().getFileOffer();
+		if (fileOffer != null) {
+			Element fileSize = fileOffer.findChild("size");
+			Element fileNameElement = fileOffer.findChild("name");
+			if (fileNameElement != null) {
+				boolean supportedFile = false;
+				String[] filename = fileNameElement.getContent()
+						.toLowerCase(Locale.US).split("\\.");
+				if (Arrays.asList(this.extensions).contains(
+						filename[filename.length - 1])) {
+					supportedFile = true;
+				} else if (Arrays.asList(this.cryptoExtensions).contains(
+						filename[filename.length - 1])) {
+					if (filename.length == 3) {
+						if (Arrays.asList(this.extensions).contains(
+								filename[filename.length - 2])) {
+							supportedFile = true;
+							if (filename[filename.length - 1].equals("otr")) {
+								Log.d(Config.LOGTAG, "receiving otr file");
+								this.message
+										.setEncryption(Message.ENCRYPTION_OTR);
+							} else {
+								this.message
+										.setEncryption(Message.ENCRYPTION_PGP);
+							}
+						}
+					}
+				}
+				if (supportedFile) {
+					long size = Long.parseLong(fileSize.getContent());
+					message.setBody(Long.toString(size));
+					conversation.add(message);
+					mXmppConnectionService.updateConversationUi();
+					if (size <= this.mJingleConnectionManager
+							.getAutoAcceptFileSize()) {
+						Log.d(Config.LOGTAG, "auto accepting file from "
+								+ packet.getFrom());
+						this.acceptedAutomatically = true;
+						this.sendAccept();
+					} else {
+						message.markUnread();
+						Log.d(Config.LOGTAG,
+								"not auto accepting new file offer with size: "
+										+ size
+										+ " allowed size:"
+										+ this.mJingleConnectionManager
+												.getAutoAcceptFileSize());
+						this.mXmppConnectionService.getNotificationService()
+								.push(message);
+					}
+					this.file = this.mXmppConnectionService.getFileBackend()
+							.getFile(message, false);
+					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+						byte[] key = conversation.getSymmetricKey();
+						if (key == null) {
+							this.sendCancel();
+							this.cancel();
+							return;
+						} else {
+							this.file.setKey(key);
+						}
+					}
+					this.file.setExpectedSize(size);
+				} else {
+					this.sendCancel();
+					this.cancel();
+				}
+			} else {
+				this.sendCancel();
+				this.cancel();
+			}
+		} else {
+			this.sendCancel();
+			this.cancel();
+		}
+	}
+
+	private void sendInitRequest() {
+		JinglePacket packet = this.bootstrapPacket("session-initiate");
+		Content content = new Content(this.contentCreator, this.contentName);
+		if (message.getType() == Message.TYPE_IMAGE) {
+			content.setTransportId(this.transportId);
+			this.file = this.mXmppConnectionService.getFileBackend().getFile(
+					message, false);
+			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+				Conversation conversation = this.message.getConversation();
+				this.mXmppConnectionService.renewSymmetricKey(conversation);
+				content.setFileOffer(this.file, true);
+				this.file.setKey(conversation.getSymmetricKey());
+			} else {
+				content.setFileOffer(this.file, false);
+			}
+			this.transportId = this.mJingleConnectionManager.nextRandomId();
+			content.setTransportId(this.transportId);
+			content.socks5transport().setChildren(getCandidatesAsElements());
+			packet.setContent(content);
+			this.sendJinglePacket(packet);
+			this.mJingleStatus = JINGLE_STATUS_INITIATED;
+		}
+	}
+
+	private List<Element> getCandidatesAsElements() {
+		List<Element> elements = new ArrayList<Element>();
+		for (JingleCandidate c : this.candidates) {
+			elements.add(c.toElement());
+		}
+		return elements;
+	}
+
+	private void sendAccept() {
+		mJingleStatus = JINGLE_STATUS_ACCEPTED;
+		this.mStatus = Downloadable.STATUS_DOWNLOADING;
+		mXmppConnectionService.updateConversationUi();
+		this.mJingleConnectionManager.getPrimaryCandidate(this.account,
+				new OnPrimaryCandidateFound() {
+
+					@Override
+					public void onPrimaryCandidateFound(boolean success,
+							final JingleCandidate candidate) {
+						final JinglePacket packet = bootstrapPacket("session-accept");
+						final Content content = new Content(contentCreator,
+								contentName);
+						content.setFileOffer(fileOffer);
+						content.setTransportId(transportId);
+						if ((success) && (!equalCandidateExists(candidate))) {
+							final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+									JingleConnection.this, candidate);
+							connections.put(candidate.getCid(), socksConnection);
+							socksConnection.connect(new OnTransportConnected() {
+
+								@Override
+								public void failed() {
+									Log.d(Config.LOGTAG,
+											"connection to our own primary candidate failed");
+									content.socks5transport().setChildren(
+											getCandidatesAsElements());
+									packet.setContent(content);
+									sendJinglePacket(packet);
+									connectNextCandidate();
+								}
+
+								@Override
+								public void established() {
+									Log.d(Config.LOGTAG,
+											"connected to primary candidate");
+									mergeCandidate(candidate);
+									content.socks5transport().setChildren(
+											getCandidatesAsElements());
+									packet.setContent(content);
+									sendJinglePacket(packet);
+									connectNextCandidate();
+								}
+							});
+						} else {
+							Log.d(Config.LOGTAG,
+									"did not find a primary candidate for ourself");
+							content.socks5transport().setChildren(
+									getCandidatesAsElements());
+							packet.setContent(content);
+							sendJinglePacket(packet);
+							connectNextCandidate();
+						}
+					}
+				});
+
+	}
+
+	private JinglePacket bootstrapPacket(String action) {
+		JinglePacket packet = new JinglePacket();
+		packet.setAction(action);
+		packet.setFrom(account.getFullJid());
+		packet.setTo(this.message.getCounterpart());
+		packet.setSessionId(this.sessionId);
+		packet.setInitiator(this.initiator);
+		return packet;
+	}
+
+	private void sendJinglePacket(JinglePacket packet) {
+		// Log.d(Config.LOGTAG,packet.toString());
+		account.getXmppConnection().sendIqPacket(packet, responseListener);
+	}
+
+	private boolean receiveAccept(JinglePacket packet) {
+		Content content = packet.getJingleContent();
+		mergeCandidates(JingleCandidate.parse(content.socks5transport()
+				.getChildren()));
+		this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
+		mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
+		this.connectNextCandidate();
+		return true;
+	}
+
+	private boolean receiveTransportInfo(JinglePacket packet) {
+		Content content = packet.getJingleContent();
+		if (content.hasSocks5Transport()) {
+			if (content.socks5transport().hasChild("activated")) {
+				if ((this.transport != null)
+						&& (this.transport instanceof JingleSocks5Transport)) {
+					onProxyActivated.success();
+				} else {
+					String cid = content.socks5transport()
+							.findChild("activated").getAttribute("cid");
+					Log.d(Config.LOGTAG, "received proxy activated (" + cid
+							+ ")prior to choosing our own transport");
+					JingleSocks5Transport connection = this.connections
+							.get(cid);
+					if (connection != null) {
+						connection.setActivated(true);
+					} else {
+						Log.d(Config.LOGTAG, "activated connection not found");
+						this.sendCancel();
+						this.cancel();
+					}
+				}
+				return true;
+			} else if (content.socks5transport().hasChild("proxy-error")) {
+				onProxyActivated.failed();
+				return true;
+			} else if (content.socks5transport().hasChild("candidate-error")) {
+				Log.d(Config.LOGTAG, "received candidate error");
+				this.receivedCandidate = true;
+				if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+						&& (this.sentCandidate)) {
+					this.connect();
+				}
+				return true;
+			} else if (content.socks5transport().hasChild("candidate-used")) {
+				String cid = content.socks5transport()
+						.findChild("candidate-used").getAttribute("cid");
+				if (cid != null) {
+					Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
+					JingleCandidate candidate = getCandidate(cid);
+					candidate.flagAsUsedByCounterpart();
+					this.receivedCandidate = true;
+					if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+							&& (this.sentCandidate)) {
+						this.connect();
+					} else {
+						Log.d(Config.LOGTAG,
+								"ignoring because file is already in transmission or we havent sent our candidate yet");
+					}
+					return true;
+				} else {
+					return false;
+				}
+			} else {
+				return false;
+			}
+		} else {
+			return true;
+		}
+	}
+
+	private void connect() {
+		final JingleSocks5Transport connection = chooseConnection();
+		this.transport = connection;
+		if (connection == null) {
+			Log.d(Config.LOGTAG, "could not find suitable candidate");
+			this.disconnect();
+			if (this.initiator.equals(account.getFullJid())) {
+				this.sendFallbackToIbb();
+			}
+		} else {
+			this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
+			if (connection.needsActivation()) {
+				if (connection.getCandidate().isOurs()) {
+					Log.d(Config.LOGTAG, "candidate "
+							+ connection.getCandidate().getCid()
+							+ " was our proxy. going to activate");
+					IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
+					activation.setTo(connection.getCandidate().getJid());
+					activation.query("http://jabber.org/protocol/bytestreams")
+							.setAttribute("sid", this.getSessionId());
+					activation.query().addChild("activate")
+							.setContent(this.getCounterPart());
+					this.account.getXmppConnection().sendIqPacket(activation,
+							new OnIqPacketReceived() {
+
+								@Override
+								public void onIqPacketReceived(Account account,
+										IqPacket packet) {
+									if (packet.getType() == IqPacket.TYPE_ERROR) {
+										onProxyActivated.failed();
+									} else {
+										onProxyActivated.success();
+										sendProxyActivated(connection
+												.getCandidate().getCid());
+									}
+								}
+							});
+				} else {
+					Log.d(Config.LOGTAG,
+							"candidate "
+									+ connection.getCandidate().getCid()
+									+ " was a proxy. waiting for other party to activate");
+				}
+			} else {
+				if (initiator.equals(account.getFullJid())) {
+					Log.d(Config.LOGTAG, "we were initiating. sending file");
+					connection.send(file, onFileTransmissionSatusChanged);
+				} else {
+					Log.d(Config.LOGTAG, "we were responding. receiving file");
+					connection.receive(file, onFileTransmissionSatusChanged);
+				}
+			}
+		}
+	}
+
+	private JingleSocks5Transport chooseConnection() {
+		JingleSocks5Transport connection = null;
+		for (Entry<String, JingleSocks5Transport> cursor : connections
+				.entrySet()) {
+			JingleSocks5Transport currentConnection = cursor.getValue();
+			// Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
+			if (currentConnection.isEstablished()
+					&& (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
+							.getCandidate().isOurs()))) {
+				// Log.d(Config.LOGTAG,"is usable");
+				if (connection == null) {
+					connection = currentConnection;
+				} else {
+					if (connection.getCandidate().getPriority() < currentConnection
+							.getCandidate().getPriority()) {
+						connection = currentConnection;
+					} else if (connection.getCandidate().getPriority() == currentConnection
+							.getCandidate().getPriority()) {
+						// Log.d(Config.LOGTAG,"found two candidates with same priority");
+						if (initiator.equals(account.getFullJid())) {
+							if (currentConnection.getCandidate().isOurs()) {
+								connection = currentConnection;
+							}
+						} else {
+							if (!currentConnection.getCandidate().isOurs()) {
+								connection = currentConnection;
+							}
+						}
+					}
+				}
+			}
+		}
+		return connection;
+	}
+
+	private void sendSuccess() {
+		JinglePacket packet = bootstrapPacket("session-terminate");
+		Reason reason = new Reason();
+		reason.addChild("success");
+		packet.setReason(reason);
+		this.sendJinglePacket(packet);
+		this.disconnect();
+		this.mJingleStatus = JINGLE_STATUS_FINISHED;
+		this.message.setStatus(Message.STATUS_RECEIVED);
+		this.message.setDownloadable(null);
+		this.mXmppConnectionService.updateMessage(message);
+		this.mJingleConnectionManager.finishConnection(this);
+	}
+
+	private void sendFallbackToIbb() {
+		Log.d(Config.LOGTAG, "sending fallback to ibb");
+		JinglePacket packet = this.bootstrapPacket("transport-replace");
+		Content content = new Content(this.contentCreator, this.contentName);
+		this.transportId = this.mJingleConnectionManager.nextRandomId();
+		content.setTransportId(this.transportId);
+		content.ibbTransport().setAttribute("block-size",
+				Integer.toString(this.ibbBlockSize));
+		packet.setContent(content);
+		this.sendJinglePacket(packet);
+	}
+
+	private boolean receiveFallbackToIbb(JinglePacket packet) {
+		Log.d(Config.LOGTAG, "receiving fallack to ibb");
+		String receivedBlockSize = packet.getJingleContent().ibbTransport()
+				.getAttribute("block-size");
+		if (receivedBlockSize != null) {
+			int bs = Integer.parseInt(receivedBlockSize);
+			if (bs > this.ibbBlockSize) {
+				this.ibbBlockSize = bs;
+			}
+		}
+		this.transportId = packet.getJingleContent().getTransportId();
+		this.transport = new JingleInbandTransport(this.account,
+				this.responder, this.transportId, this.ibbBlockSize);
+		this.transport.receive(file, onFileTransmissionSatusChanged);
+		JinglePacket answer = bootstrapPacket("transport-accept");
+		Content content = new Content("initiator", "a-file-offer");
+		content.setTransportId(this.transportId);
+		content.ibbTransport().setAttribute("block-size",
+				Integer.toString(this.ibbBlockSize));
+		answer.setContent(content);
+		this.sendJinglePacket(answer);
+		return true;
+	}
+
+	private boolean receiveTransportAccept(JinglePacket packet) {
+		if (packet.getJingleContent().hasIbbTransport()) {
+			String receivedBlockSize = packet.getJingleContent().ibbTransport()
+					.getAttribute("block-size");
+			if (receivedBlockSize != null) {
+				int bs = Integer.parseInt(receivedBlockSize);
+				if (bs > this.ibbBlockSize) {
+					this.ibbBlockSize = bs;
+				}
+			}
+			this.transport = new JingleInbandTransport(this.account,
+					this.responder, this.transportId, this.ibbBlockSize);
+			this.transport.connect(new OnTransportConnected() {
+
+				@Override
+				public void failed() {
+					Log.d(Config.LOGTAG, "ibb open failed");
+				}
+
+				@Override
+				public void established() {
+					JingleConnection.this.transport.send(file,
+							onFileTransmissionSatusChanged);
+				}
+			});
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	private void receiveSuccess() {
+		this.mJingleStatus = JINGLE_STATUS_FINISHED;
+		this.mXmppConnectionService.markMessage(this.message,
+				Message.STATUS_SEND);
+		this.disconnect();
+		this.mJingleConnectionManager.finishConnection(this);
+	}
+
+	public void cancel() {
+		this.mJingleStatus = JINGLE_STATUS_CANCELED;
+		this.disconnect();
+		if (this.message != null) {
+			if (this.responder.equals(account.getFullJid())) {
+				this.mStatus = Downloadable.STATUS_FAILED;
+				this.mXmppConnectionService.updateConversationUi();
+			} else {
+				if (this.mJingleStatus == JINGLE_STATUS_INITIATED) {
+					this.mXmppConnectionService.markMessage(this.message,
+							Message.STATUS_SEND_REJECTED);
+				} else {
+					this.mXmppConnectionService.markMessage(this.message,
+							Message.STATUS_SEND_FAILED);
+				}
+			}
+		}
+		this.mJingleConnectionManager.finishConnection(this);
+	}
+
+	private void sendCancel() {
+		JinglePacket packet = bootstrapPacket("session-terminate");
+		Reason reason = new Reason();
+		reason.addChild("cancel");
+		packet.setReason(reason);
+		this.sendJinglePacket(packet);
+	}
+
+	private void connectNextCandidate() {
+		for (JingleCandidate candidate : this.candidates) {
+			if ((!connections.containsKey(candidate.getCid()) && (!candidate
+					.isOurs()))) {
+				this.connectWithCandidate(candidate);
+				return;
+			}
+		}
+		this.sendCandidateError();
+	}
+
+	private void connectWithCandidate(final JingleCandidate candidate) {
+		final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+				this, candidate);
+		connections.put(candidate.getCid(), socksConnection);
+		socksConnection.connect(new OnTransportConnected() {
+
+			@Override
+			public void failed() {
+				Log.d(Config.LOGTAG,
+						"connection failed with " + candidate.getHost() + ":"
+								+ candidate.getPort());
+				connectNextCandidate();
+			}
+
+			@Override
+			public void established() {
+				Log.d(Config.LOGTAG,
+						"established connection with " + candidate.getHost()
+								+ ":" + candidate.getPort());
+				sendCandidateUsed(candidate.getCid());
+			}
+		});
+	}
+
+	private void disconnect() {
+		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
+				.entrySet().iterator();
+		while (it.hasNext()) {
+			Entry<String, JingleSocks5Transport> pairs = it.next();
+			pairs.getValue().disconnect();
+			it.remove();
+		}
+	}
+
+	private void sendProxyActivated(String cid) {
+		JinglePacket packet = bootstrapPacket("transport-info");
+		Content content = new Content(this.contentCreator, this.contentName);
+		content.setTransportId(this.transportId);
+		content.socks5transport().addChild("activated")
+				.setAttribute("cid", cid);
+		packet.setContent(content);
+		this.sendJinglePacket(packet);
+	}
+
+	private void sendCandidateUsed(final String cid) {
+		JinglePacket packet = bootstrapPacket("transport-info");
+		Content content = new Content(this.contentCreator, this.contentName);
+		content.setTransportId(this.transportId);
+		content.socks5transport().addChild("candidate-used")
+				.setAttribute("cid", cid);
+		packet.setContent(content);
+		this.sentCandidate = true;
+		if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+			connect();
+		}
+		this.sendJinglePacket(packet);
+	}
+
+	private void sendCandidateError() {
+		JinglePacket packet = bootstrapPacket("transport-info");
+		Content content = new Content(this.contentCreator, this.contentName);
+		content.setTransportId(this.transportId);
+		content.socks5transport().addChild("candidate-error");
+		packet.setContent(content);
+		this.sentCandidate = true;
+		if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+			connect();
+		}
+		this.sendJinglePacket(packet);
+	}
+
+	public String getInitiator() {
+		return this.initiator;
+	}
+
+	public String getResponder() {
+		return this.responder;
+	}
+
+	public int getJingleStatus() {
+		return this.mJingleStatus;
+	}
+
+	private boolean equalCandidateExists(JingleCandidate candidate) {
+		for (JingleCandidate c : this.candidates) {
+			if (c.equalValues(candidate)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private void mergeCandidate(JingleCandidate candidate) {
+		for (JingleCandidate c : this.candidates) {
+			if (c.equals(candidate)) {
+				return;
+			}
+		}
+		this.candidates.add(candidate);
+	}
+
+	private void mergeCandidates(List<JingleCandidate> candidates) {
+		for (JingleCandidate c : candidates) {
+			mergeCandidate(c);
+		}
+	}
+
+	private JingleCandidate getCandidate(String cid) {
+		for (JingleCandidate c : this.candidates) {
+			if (c.getCid().equals(cid)) {
+				return c;
+			}
+		}
+		return null;
+	}
+
+	interface OnProxyActivated {
+		public void success();
+
+		public void failed();
+	}
+
+	public boolean hasTransportId(String sid) {
+		return sid.equals(this.transportId);
+	}
+
+	public JingleTransport getTransport() {
+		return this.transport;
+	}
+
+	public boolean start() {
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			if (mJingleStatus == JINGLE_STATUS_INITIATED) {
+				new Thread(new Runnable() {
+
+					@Override
+					public void run() {
+						sendAccept();
+					}
+				}).start();
+			}
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	@Override
+	public int getStatus() {
+		return this.mStatus;
+	}
+
+	@Override
+	public long getFileSize() {
+		if (this.file != null) {
+			return this.file.getExpectedSize();
+		} else {
+			return 0;
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -0,0 +1,163 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import android.annotation.SuppressLint;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnectionManager extends AbstractConnectionManager {
+	private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>();
+
+	private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>();
+
+	@SuppressLint("TrulyRandom")
+	private SecureRandom random = new SecureRandom();
+
+	public JingleConnectionManager(XmppConnectionService service) {
+		super(service);
+	}
+
+	public void deliverPacket(Account account, JinglePacket packet) {
+		if (packet.isAction("session-initiate")) {
+			JingleConnection connection = new JingleConnection(this);
+			connection.init(account, packet);
+			connections.add(connection);
+		} else {
+			for (JingleConnection connection : connections) {
+				if (connection.getAccount() == account
+						&& connection.getSessionId().equals(
+								packet.getSessionId())
+						&& connection.getCounterPart().equals(packet.getFrom())) {
+					connection.deliverPacket(packet);
+					return;
+				}
+			}
+			account.getXmppConnection().sendIqPacket(
+					packet.generateRespone(IqPacket.TYPE_ERROR), null);
+		}
+	}
+
+	public JingleConnection createNewConnection(Message message) {
+		JingleConnection connection = new JingleConnection(this);
+		connection.init(message);
+		this.connections.add(connection);
+		return connection;
+	}
+
+	public JingleConnection createNewConnection(JinglePacket packet) {
+		JingleConnection connection = new JingleConnection(this);
+		this.connections.add(connection);
+		return connection;
+	}
+
+	public void finishConnection(JingleConnection connection) {
+		this.connections.remove(connection);
+	}
+
+	public void getPrimaryCandidate(Account account,
+			final OnPrimaryCandidateFound listener) {
+		if (!this.primaryCandidates.containsKey(account.getJid())) {
+			String xmlns = "http://jabber.org/protocol/bytestreams";
+			final String proxy = account.getXmppConnection()
+					.findDiscoItemByFeature(xmlns);
+			if (proxy != null) {
+				IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+				iq.setTo(proxy);
+				iq.query(xmlns);
+				account.getXmppConnection().sendIqPacket(iq,
+						new OnIqPacketReceived() {
+
+							@Override
+							public void onIqPacketReceived(Account account,
+									IqPacket packet) {
+								Element streamhost = packet
+										.query()
+										.findChild("streamhost",
+												"http://jabber.org/protocol/bytestreams");
+								if (streamhost != null) {
+									JingleCandidate candidate = new JingleCandidate(
+											nextRandomId(), true);
+									candidate.setHost(streamhost
+											.getAttribute("host"));
+									candidate.setPort(Integer
+											.parseInt(streamhost
+													.getAttribute("port")));
+									candidate
+											.setType(JingleCandidate.TYPE_PROXY);
+									candidate.setJid(proxy);
+									candidate.setPriority(655360 + 65535);
+									primaryCandidates.put(account.getJid(),
+											candidate);
+									listener.onPrimaryCandidateFound(true,
+											candidate);
+								} else {
+									listener.onPrimaryCandidateFound(false,
+											null);
+								}
+							}
+						});
+			} else {
+				listener.onPrimaryCandidateFound(false, null);
+			}
+
+		} else {
+			listener.onPrimaryCandidateFound(true,
+					this.primaryCandidates.get(account.getJid()));
+		}
+	}
+
+	public String nextRandomId() {
+		return new BigInteger(50, random).toString(32);
+	}
+
+	public void deliverIbbPacket(Account account, IqPacket packet) {
+		String sid = null;
+		Element payload = null;
+		if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) {
+			payload = packet
+					.findChild("open", "http://jabber.org/protocol/ibb");
+			sid = payload.getAttribute("sid");
+		} else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
+			payload = packet
+					.findChild("data", "http://jabber.org/protocol/ibb");
+			sid = payload.getAttribute("sid");
+		}
+		if (sid != null) {
+			for (JingleConnection connection : connections) {
+				if (connection.getAccount() == account
+						&& connection.hasTransportId(sid)) {
+					JingleTransport transport = connection.getTransport();
+					if (transport instanceof JingleInbandTransport) {
+						JingleInbandTransport inbandTransport = (JingleInbandTransport) transport;
+						inbandTransport.deliverPayload(packet, payload);
+						return;
+					}
+				}
+			}
+			Log.d(Config.LOGTAG,
+					"couldnt deliver payload: " + payload.toString());
+		} else {
+			Log.d(Config.LOGTAG, "no sid found in incomming ibb packet");
+		}
+	}
+
+	public void cancelInTransmission() {
+		for (JingleConnection connection : this.connections) {
+			if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
+				connection.cancel();
+			}
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java 🔗

@@ -0,0 +1,191 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import android.util.Base64;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleInbandTransport extends JingleTransport {
+
+	private Account account;
+	private String counterpart;
+	private int blockSize;
+	private int bufferSize;
+	private int seq = 0;
+	private String sessionId;
+
+	private boolean established = false;
+
+	private DownloadableFile file;
+
+	private InputStream fileInputStream = null;
+	private OutputStream fileOutputStream;
+	private long remainingSize;
+	private MessageDigest digest;
+
+	private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
+
+	private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
+		@Override
+		public void onIqPacketReceived(Account account, IqPacket packet) {
+			if (packet.getType() == IqPacket.TYPE_RESULT) {
+				sendNextBlock();
+			}
+		}
+	};
+
+	public JingleInbandTransport(Account account, String counterpart,
+			String sid, int blocksize) {
+		this.account = account;
+		this.counterpart = counterpart;
+		this.blockSize = blocksize;
+		this.bufferSize = blocksize / 4;
+		this.sessionId = sid;
+	}
+
+	public void connect(final OnTransportConnected callback) {
+		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+		iq.setTo(this.counterpart);
+		Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
+		open.setAttribute("sid", this.sessionId);
+		open.setAttribute("stanza", "iq");
+		open.setAttribute("block-size", Integer.toString(this.blockSize));
+
+		this.account.getXmppConnection().sendIqPacket(iq,
+				new OnIqPacketReceived() {
+
+					@Override
+					public void onIqPacketReceived(Account account,
+							IqPacket packet) {
+						if (packet.getType() == IqPacket.TYPE_ERROR) {
+							callback.failed();
+						} else {
+							callback.established();
+						}
+					}
+				});
+	}
+
+	@Override
+	public void receive(DownloadableFile file,
+			OnFileTransmissionStatusChanged callback) {
+		this.onFileTransmissionStatusChanged = callback;
+		this.file = file;
+		try {
+			this.digest = MessageDigest.getInstance("SHA-1");
+			digest.reset();
+			file.getParentFile().mkdirs();
+			file.createNewFile();
+			this.fileOutputStream = file.createOutputStream();
+			if (this.fileOutputStream == null) {
+				callback.onFileTransferAborted();
+				return;
+			}
+			this.remainingSize = file.getExpectedSize();
+		} catch (NoSuchAlgorithmException e) {
+			callback.onFileTransferAborted();
+		} catch (IOException e) {
+			callback.onFileTransferAborted();
+		}
+	}
+
+	@Override
+	public void send(DownloadableFile file,
+			OnFileTransmissionStatusChanged callback) {
+		this.onFileTransmissionStatusChanged = callback;
+		this.file = file;
+		try {
+			this.digest = MessageDigest.getInstance("SHA-1");
+			this.digest.reset();
+			fileInputStream = this.file.createInputStream();
+			if (fileInputStream == null) {
+				callback.onFileTransferAborted();
+				return;
+			}
+			this.sendNextBlock();
+		} catch (NoSuchAlgorithmException e) {
+			callback.onFileTransferAborted();
+		}
+	}
+
+	private void sendNextBlock() {
+		byte[] buffer = new byte[this.bufferSize];
+		try {
+			int count = fileInputStream.read(buffer);
+			if (count == -1) {
+				file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+				fileInputStream.close();
+				this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+			} else {
+				this.digest.update(buffer);
+				String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP);
+				IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+				iq.setTo(this.counterpart);
+				Element data = iq.addChild("data",
+						"http://jabber.org/protocol/ibb");
+				data.setAttribute("seq", Integer.toString(this.seq));
+				data.setAttribute("block-size",
+						Integer.toString(this.blockSize));
+				data.setAttribute("sid", this.sessionId);
+				data.setContent(base64);
+				this.account.getXmppConnection().sendIqPacket(iq,
+						this.onAckReceived);
+				this.seq++;
+			}
+		} catch (IOException e) {
+			this.onFileTransmissionStatusChanged.onFileTransferAborted();
+		}
+	}
+
+	private void receiveNextBlock(String data) {
+		try {
+			byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
+			if (this.remainingSize < buffer.length) {
+				buffer = Arrays
+						.copyOfRange(buffer, 0, (int) this.remainingSize);
+			}
+			this.remainingSize -= buffer.length;
+
+			this.fileOutputStream.write(buffer);
+
+			this.digest.update(buffer);
+			if (this.remainingSize <= 0) {
+				file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+				fileOutputStream.flush();
+				fileOutputStream.close();
+				this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+			}
+		} catch (IOException e) {
+			this.onFileTransmissionStatusChanged.onFileTransferAborted();
+		}
+	}
+
+	public void deliverPayload(IqPacket packet, Element payload) {
+		if (payload.getName().equals("open")) {
+			if (!established) {
+				established = true;
+				this.account.getXmppConnection().sendIqPacket(
+						packet.generateRespone(IqPacket.TYPE_RESULT), null);
+			} else {
+				this.account.getXmppConnection().sendIqPacket(
+						packet.generateRespone(IqPacket.TYPE_ERROR), null);
+			}
+		} else if (payload.getName().equals("data")) {
+			this.receiveNextBlock(payload.getContent());
+			this.account.getXmppConnection().sendIqPacket(
+					packet.generateRespone(IqPacket.TYPE_RESULT), null);
+		} else {
+			// TODO some sort of exception
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java 🔗

@@ -0,0 +1,212 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class JingleSocks5Transport extends JingleTransport {
+	private JingleCandidate candidate;
+	private String destination;
+	private OutputStream outputStream;
+	private InputStream inputStream;
+	private boolean isEstablished = false;
+	private boolean activated = false;
+	protected Socket socket;
+
+	public JingleSocks5Transport(JingleConnection jingleConnection,
+			JingleCandidate candidate) {
+		this.candidate = candidate;
+		try {
+			MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
+			StringBuilder destBuilder = new StringBuilder();
+			destBuilder.append(jingleConnection.getSessionId());
+			if (candidate.isOurs()) {
+				destBuilder.append(jingleConnection.getAccount().getFullJid());
+				destBuilder.append(jingleConnection.getCounterPart());
+			} else {
+				destBuilder.append(jingleConnection.getCounterPart());
+				destBuilder.append(jingleConnection.getAccount().getFullJid());
+			}
+			mDigest.reset();
+			this.destination = CryptoHelper.bytesToHex(mDigest
+					.digest(destBuilder.toString().getBytes()));
+		} catch (NoSuchAlgorithmException e) {
+
+		}
+	}
+
+	public void connect(final OnTransportConnected callback) {
+		new Thread(new Runnable() {
+
+			@Override
+			public void run() {
+				try {
+					socket = new Socket(candidate.getHost(),
+							candidate.getPort());
+					inputStream = socket.getInputStream();
+					outputStream = socket.getOutputStream();
+					byte[] login = { 0x05, 0x01, 0x00 };
+					byte[] expectedReply = { 0x05, 0x00 };
+					byte[] reply = new byte[2];
+					outputStream.write(login);
+					inputStream.read(reply);
+					final String connect = Character.toString('\u0005')
+							+ '\u0001' + '\u0000' + '\u0003' + '\u0028'
+							+ destination + '\u0000' + '\u0000';
+					if (Arrays.equals(reply, expectedReply)) {
+						outputStream.write(connect.getBytes());
+						byte[] result = new byte[2];
+						inputStream.read(result);
+						int status = result[1];
+						if (status == 0) {
+							isEstablished = true;
+							callback.established();
+						} else {
+							callback.failed();
+						}
+					} else {
+						socket.close();
+						callback.failed();
+					}
+				} catch (UnknownHostException e) {
+					callback.failed();
+				} catch (IOException e) {
+					callback.failed();
+				}
+			}
+		}).start();
+
+	}
+
+	public void send(final DownloadableFile file,
+			final OnFileTransmissionStatusChanged callback) {
+		new Thread(new Runnable() {
+
+			@Override
+			public void run() {
+				InputStream fileInputStream = null;
+				try {
+					MessageDigest digest = MessageDigest.getInstance("SHA-1");
+					digest.reset();
+					fileInputStream = file.createInputStream();
+					if (fileInputStream == null) {
+						callback.onFileTransferAborted();
+						return;
+					}
+					int count;
+					byte[] buffer = new byte[8192];
+					while ((count = fileInputStream.read(buffer)) > 0) {
+						outputStream.write(buffer, 0, count);
+						digest.update(buffer, 0, count);
+					}
+					outputStream.flush();
+					file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+					if (callback != null) {
+						callback.onFileTransmitted(file);
+					}
+				} catch (FileNotFoundException e) {
+					callback.onFileTransferAborted();
+				} catch (IOException e) {
+					callback.onFileTransferAborted();
+				} catch (NoSuchAlgorithmException e) {
+					callback.onFileTransferAborted();
+				} finally {
+					try {
+						if (fileInputStream != null) {
+							fileInputStream.close();
+						}
+					} catch (IOException e) {
+						callback.onFileTransferAborted();
+					}
+				}
+			}
+		}).start();
+
+	}
+
+	public void receive(final DownloadableFile file,
+			final OnFileTransmissionStatusChanged callback) {
+		new Thread(new Runnable() {
+
+			@Override
+			public void run() {
+				try {
+					MessageDigest digest = MessageDigest.getInstance("SHA-1");
+					digest.reset();
+					inputStream.skip(45);
+					socket.setSoTimeout(30000);
+					file.getParentFile().mkdirs();
+					file.createNewFile();
+					OutputStream fileOutputStream = file.createOutputStream();
+					if (fileOutputStream == null) {
+						callback.onFileTransferAborted();
+						return;
+					}
+					long remainingSize = file.getExpectedSize();
+					byte[] buffer = new byte[8192];
+					int count = buffer.length;
+					while (remainingSize > 0) {
+						count = inputStream.read(buffer);
+						if (count == -1) {
+							callback.onFileTransferAborted();
+							return;
+						} else {
+							fileOutputStream.write(buffer, 0, count);
+							digest.update(buffer, 0, count);
+							remainingSize -= count;
+						}
+					}
+					fileOutputStream.flush();
+					fileOutputStream.close();
+					file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+					callback.onFileTransmitted(file);
+				} catch (FileNotFoundException e) {
+					callback.onFileTransferAborted();
+				} catch (IOException e) {
+					callback.onFileTransferAborted();
+				} catch (NoSuchAlgorithmException e) {
+					callback.onFileTransferAborted();
+				}
+			}
+		}).start();
+	}
+
+	public boolean isProxy() {
+		return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
+	}
+
+	public boolean needsActivation() {
+		return (this.isProxy() && !this.activated);
+	}
+
+	public void disconnect() {
+		if (this.socket != null) {
+			try {
+				this.socket.close();
+			} catch (IOException e) {
+
+			}
+		}
+	}
+
+	public boolean isEstablished() {
+		return this.isEstablished;
+	}
+
+	public JingleCandidate getCandidate() {
+		return this.candidate;
+	}
+
+	public void setActivated(boolean activated) {
+		this.activated = activated;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java 🔗

@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public abstract class JingleTransport {
+	public abstract void connect(final OnTransportConnected callback);
+
+	public abstract void receive(final DownloadableFile file,
+			final OnFileTransmissionStatusChanged callback);
+
+	public abstract void send(final DownloadableFile file,
+			final OnFileTransmissionStatusChanged callback);
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java 🔗

@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.PacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+
+public interface OnJinglePacketReceived extends PacketReceived {
+	public void onJinglePacketReceived(Account account, JinglePacket packet);
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java 🔗

@@ -0,0 +1,102 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Element;
+
+public class Content extends Element {
+
+	private String transportId;
+
+	private Content(String name) {
+		super(name);
+	}
+
+	public Content() {
+		super("content");
+	}
+
+	public Content(String creator, String name) {
+		super("content");
+		this.setAttribute("creator", creator);
+		this.setAttribute("name", name);
+	}
+
+	public void setTransportId(String sid) {
+		this.transportId = sid;
+	}
+
+	public void setFileOffer(DownloadableFile actualFile, boolean otr) {
+		Element description = this.addChild("description",
+				"urn:xmpp:jingle:apps:file-transfer:3");
+		Element offer = description.addChild("offer");
+		Element file = offer.addChild("file");
+		file.addChild("size").setContent(Long.toString(actualFile.getSize()));
+		if (otr) {
+			file.addChild("name").setContent(actualFile.getName() + ".otr");
+		} else {
+			file.addChild("name").setContent(actualFile.getName());
+		}
+	}
+
+	public Element getFileOffer() {
+		Element description = this.findChild("description",
+				"urn:xmpp:jingle:apps:file-transfer:3");
+		if (description == null) {
+			return null;
+		}
+		Element offer = description.findChild("offer");
+		if (offer == null) {
+			return null;
+		}
+		return offer.findChild("file");
+	}
+
+	public void setFileOffer(Element fileOffer) {
+		Element description = this.findChild("description",
+				"urn:xmpp:jingle:apps:file-transfer:3");
+		if (description == null) {
+			description = this.addChild("description",
+					"urn:xmpp:jingle:apps:file-transfer:3");
+		}
+		description.addChild(fileOffer);
+	}
+
+	public String getTransportId() {
+		if (hasSocks5Transport()) {
+			this.transportId = socks5transport().getAttribute("sid");
+		} else if (hasIbbTransport()) {
+			this.transportId = ibbTransport().getAttribute("sid");
+		}
+		return this.transportId;
+	}
+
+	public Element socks5transport() {
+		Element transport = this.findChild("transport",
+				"urn:xmpp:jingle:transports:s5b:1");
+		if (transport == null) {
+			transport = this.addChild("transport",
+					"urn:xmpp:jingle:transports:s5b:1");
+			transport.setAttribute("sid", this.transportId);
+		}
+		return transport;
+	}
+
+	public Element ibbTransport() {
+		Element transport = this.findChild("transport",
+				"urn:xmpp:jingle:transports:ibb:1");
+		if (transport == null) {
+			transport = this.addChild("transport",
+					"urn:xmpp:jingle:transports:ibb:1");
+			transport.setAttribute("sid", this.transportId);
+		}
+		return transport;
+	}
+
+	public boolean hasSocks5Transport() {
+		return this.hasChild("transport", "urn:xmpp:jingle:transports:s5b:1");
+	}
+
+	public boolean hasIbbTransport() {
+		return this.hasChild("transport", "urn:xmpp:jingle:transports:ibb:1");
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java 🔗

@@ -0,0 +1,95 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JinglePacket extends IqPacket {
+	Content content = null;
+	Reason reason = null;
+	Element jingle = new Element("jingle");
+
+	@Override
+	public Element addChild(Element child) {
+		if ("jingle".equals(child.getName())) {
+			Element contentElement = child.findChild("content");
+			if (contentElement != null) {
+				this.content = new Content();
+				this.content.setChildren(contentElement.getChildren());
+				this.content.setAttributes(contentElement.getAttributes());
+			}
+			Element reasonElement = child.findChild("reason");
+			if (reasonElement != null) {
+				this.reason = new Reason();
+				this.reason.setChildren(reasonElement.getChildren());
+				this.reason.setAttributes(reasonElement.getAttributes());
+			}
+			this.jingle.setAttributes(child.getAttributes());
+		}
+		return child;
+	}
+
+	public JinglePacket setContent(Content content) {
+		this.content = content;
+		return this;
+	}
+
+	public Content getJingleContent() {
+		if (this.content == null) {
+			this.content = new Content();
+		}
+		return this.content;
+	}
+
+	public JinglePacket setReason(Reason reason) {
+		this.reason = reason;
+		return this;
+	}
+
+	public Reason getReason() {
+		return this.reason;
+	}
+
+	private void build() {
+		this.children.clear();
+		this.jingle.clearChildren();
+		this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1");
+		if (this.content != null) {
+			jingle.addChild(this.content);
+		}
+		if (this.reason != null) {
+			jingle.addChild(this.reason);
+		}
+		this.children.add(jingle);
+		this.setAttribute("type", "set");
+	}
+
+	public String getSessionId() {
+		return this.jingle.getAttribute("sid");
+	}
+
+	public void setSessionId(String sid) {
+		this.jingle.setAttribute("sid", sid);
+	}
+
+	@Override
+	public String toString() {
+		this.build();
+		return super.toString();
+	}
+
+	public void setAction(String action) {
+		this.jingle.setAttribute("action", action);
+	}
+
+	public String getAction() {
+		return this.jingle.getAttribute("action");
+	}
+
+	public void setInitiator(String initiator) {
+		this.jingle.setAttribute("initiator", initiator);
+	}
+
+	public boolean isAction(String action) {
+		return action.equalsIgnoreCase(this.getAction());
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java 🔗

@@ -0,0 +1,71 @@
+package eu.siacs.conversations.xmpp.pep;
+
+import eu.siacs.conversations.xml.Element;
+import android.util.Base64;
+
+public class Avatar {
+	public String type;
+	public String sha1sum;
+	public String image;
+	public int height;
+	public int width;
+	public long size;
+	public String owner;
+
+	public byte[] getImageAsBytes() {
+		return Base64.decode(image, Base64.DEFAULT);
+	}
+
+	public String getFilename() {
+		if (type == null) {
+			return sha1sum;
+		} else if (type.equalsIgnoreCase("image/webp")) {
+			return sha1sum + ".webp";
+		} else if (type.equalsIgnoreCase("image/png")) {
+			return sha1sum + ".png";
+		} else {
+			return sha1sum;
+		}
+	}
+
+	public static Avatar parseMetadata(Element items) {
+		Element item = items.findChild("item");
+		if (item == null) {
+			return null;
+		}
+		Element metadata = item.findChild("metadata");
+		if (metadata == null) {
+			return null;
+		}
+		String primaryId = item.getAttribute("id");
+		if (primaryId == null) {
+			return null;
+		}
+		for (Element child : metadata.getChildren()) {
+			if (child.getName().equals("info")
+					&& primaryId.equals(child.getAttribute("id"))) {
+				Avatar avatar = new Avatar();
+				String height = child.getAttribute("height");
+				String width = child.getAttribute("width");
+				String size = child.getAttribute("bytes");
+				try {
+					if (height != null) {
+						avatar.height = Integer.parseInt(height);
+					}
+					if (width != null) {
+						avatar.width = Integer.parseInt(width);
+					}
+					if (size != null) {
+						avatar.size = Long.parseLong(size);
+					}
+				} catch (NumberFormatException e) {
+					return null;
+				}
+				avatar.type = child.getAttribute("type");
+				avatar.sha1sum = child.getAttribute("id");
+				return avatar;
+			}
+		}
+		return null;
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java 🔗

@@ -0,0 +1,34 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class AbstractStanza extends Element {
+
+	protected AbstractStanza(String name) {
+		super(name);
+	}
+
+	public String getTo() {
+		return getAttribute("to");
+	}
+
+	public String getFrom() {
+		return getAttribute("from");
+	}
+
+	public String getId() {
+		return this.getAttribute("id");
+	}
+
+	public void setTo(String to) {
+		setAttribute("to", to);
+	}
+
+	public void setFrom(String from) {
+		setAttribute("from", from);
+	}
+
+	public void setId(String id) {
+		setAttribute("id", id);
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java 🔗

@@ -0,0 +1,76 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class IqPacket extends AbstractStanza {
+
+	public static final int TYPE_ERROR = -1;
+	public static final int TYPE_SET = 0;
+	public static final int TYPE_RESULT = 1;
+	public static final int TYPE_GET = 2;
+
+	private IqPacket(String name) {
+		super(name);
+	}
+
+	public IqPacket(int type) {
+		super("iq");
+		switch (type) {
+		case TYPE_SET:
+			this.setAttribute("type", "set");
+			break;
+		case TYPE_GET:
+			this.setAttribute("type", "get");
+			break;
+		case TYPE_RESULT:
+			this.setAttribute("type", "result");
+			break;
+		case TYPE_ERROR:
+			this.setAttribute("type", "error");
+			break;
+		default:
+			break;
+		}
+	}
+
+	public IqPacket() {
+		super("iq");
+	}
+
+	public Element query() {
+		Element query = findChild("query");
+		if (query == null) {
+			query = addChild("query");
+		}
+		return query;
+	}
+
+	public Element query(String xmlns) {
+		Element query = query();
+		query.setAttribute("xmlns", xmlns);
+		return query();
+	}
+
+	public int getType() {
+		String type = getAttribute("type");
+		if ("error".equals(type)) {
+			return TYPE_ERROR;
+		} else if ("result".equals(type)) {
+			return TYPE_RESULT;
+		} else if ("set".equals(type)) {
+			return TYPE_SET;
+		} else if ("get".equals(type)) {
+			return TYPE_GET;
+		} else {
+			return 1000;
+		}
+	}
+
+	public IqPacket generateRespone(int type) {
+		IqPacket packet = new IqPacket(type);
+		packet.setTo(this.getFrom());
+		packet.setId(this.getId());
+		return packet;
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java 🔗

@@ -0,0 +1,66 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class MessagePacket extends AbstractStanza {
+	public static final int TYPE_CHAT = 0;
+	public static final int TYPE_NORMAL = 2;
+	public static final int TYPE_GROUPCHAT = 3;
+	public static final int TYPE_ERROR = 4;
+	public static final int TYPE_HEADLINE = 5;
+
+	public MessagePacket() {
+		super("message");
+	}
+
+	public String getBody() {
+		Element body = this.findChild("body");
+		if (body != null) {
+			return body.getContent();
+		} else {
+			return null;
+		}
+	}
+
+	public void setBody(String text) {
+		this.children.remove(findChild("body"));
+		Element body = new Element("body");
+		body.setContent(text);
+		this.children.add(body);
+	}
+
+	public void setType(int type) {
+		switch (type) {
+		case TYPE_CHAT:
+			this.setAttribute("type", "chat");
+			break;
+		case TYPE_GROUPCHAT:
+			this.setAttribute("type", "groupchat");
+			break;
+		case TYPE_NORMAL:
+			break;
+		default:
+			this.setAttribute("type", "chat");
+			break;
+		}
+	}
+
+	public int getType() {
+		String type = getAttribute("type");
+		if (type == null) {
+			return TYPE_NORMAL;
+		} else if (type.equals("normal")) {
+			return TYPE_NORMAL;
+		} else if (type.equals("chat")) {
+			return TYPE_CHAT;
+		} else if (type.equals("groupchat")) {
+			return TYPE_GROUPCHAT;
+		} else if (type.equals("error")) {
+			return TYPE_ERROR;
+		} else if (type.equals("headline")) {
+			return TYPE_HEADLINE;
+		} else {
+			return TYPE_NORMAL;
+		}
+	}
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java 🔗

@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class AckPacket extends AbstractStanza {
+
+	public AckPacket(int sequence, int smVersion) {
+		super("a");
+		this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+		this.setAttribute("h", Integer.toString(sequence));
+	}
+
+}

conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java 🔗

@@ -0,0 +1,14 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class ResumePacket extends AbstractStanza {
+
+	public ResumePacket(String id, int sequence, int smVersion) {
+		super("resume");
+		this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+		this.setAttribute("previd", id);
+		this.setAttribute("h", Integer.toString(sequence));
+	}
+
+}

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/src/main/res/layout/message_received.xml 🔗

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingBottom="4dp"
+    android:paddingLeft="8dp"
+    android:paddingRight="8dp"
+    android:paddingTop="4dp" >
+
+    <LinearLayout
+        android:id="@+id/message_box"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_toRightOf="@+id/message_photo"
+        android:background="@drawable/message_border"
+        android:minHeight="48dp" >
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="fill_parent"
+            android:background="@color/primarybackground"
+            android:gravity="center_vertical"
+            android:orientation="vertical"
+            android:paddingBottom="4dp"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:paddingTop="4dp" >
+
+            <ImageView
+                android:id="@+id/message_image"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:adjustViewBounds="true"
+                android:background="@color/primarytext"
+                android:paddingBottom="2dp"
+                android:scaleType="centerCrop" />
+
+            <TextView
+                android:id="@+id/message_body"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:autoLink="web"
+                android:textColor="@color/primarytext"
+                android:textIsSelectable="true"
+                android:textSize="?attr/TextSizeBody" />
+
+            <Button
+                android:id="@+id/download_button"
+                style="?android:attr/buttonStyleSmall"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/download_image"
+                android:visibility="gone" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:paddingTop="1dp" >
+
+                <ImageView
+                    android:id="@+id/security_indicator"
+                    android:layout_width="?attr/TextSizeInfo"
+                    android:layout_height="?attr/TextSizeInfo"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginRight="4sp"
+                    android:alpha="0.54"
+                    android:gravity="center_vertical"
+                    android:src="@drawable/ic_secure_indicator" />
+
+                <TextView
+                    android:id="@+id/message_time"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:gravity="center_vertical"
+                    android:text="@string/sending"
+                    android:textColor="@color/secondarytext"
+                    android:textSize="?attr/TextSizeInfo" />
+            </LinearLayout>
+        </LinearLayout>
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/message_photo"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:layout_marginRight="-1.5dp"
+        android:padding="0dp"
+        android:scaleType="fitXY"
+        android:src="@drawable/ic_profile" />
+
+</RelativeLayout>

conversations/src/main/res/layout/message_sent.xml 🔗

@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingBottom="4dp"
+    android:paddingLeft="8dp"
+    android:paddingRight="8dp"
+    android:paddingTop="4dp" >
+
+    <LinearLayout
+        android:id="@+id/message_box"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_toLeftOf="@+id/message_photo"
+        android:background="@drawable/message_border"
+        android:minHeight="48dp" >
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="fill_parent"
+            android:background="@color/primarybackground"
+            android:gravity="center_vertical"
+            android:orientation="vertical"
+            android:paddingBottom="4dp"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:paddingTop="4dp" >
+
+            <ImageView
+                android:id="@+id/message_image"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:adjustViewBounds="true"
+                android:background="@color/primarytext"
+                android:paddingBottom="2dp"
+                android:scaleType="centerCrop" />
+
+            <TextView
+                android:id="@+id/message_body"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:autoLink="web"
+                android:textColor="@color/primarytext"
+                android:textIsSelectable="true"
+                android:textSize="?attr/TextSizeBody" />
+            
+             <Button
+                android:id="@+id/download_button"
+                style="?android:attr/buttonStyleSmall"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/download_image"
+                android:visibility="gone" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="right"
+                android:orientation="horizontal"
+                android:paddingTop="1dp" >
+
+                <TextView
+                    android:id="@+id/message_time"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:gravity="center_vertical"
+                    android:text="@string/sending"
+                    android:textColor="@color/secondarytext"
+                    android:textSize="?attr/TextSizeInfo" />
+
+                <ImageView
+                    android:id="@+id/security_indicator"
+                    android:layout_width="?attr/TextSizeInfo"
+                    android:layout_height="?attr/TextSizeInfo"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginLeft="4sp"
+                    android:alpha="0.54"
+                    android:gravity="center_vertical"
+                    android:src="@drawable/ic_secure_indicator" />
+
+                <ImageView
+                    android:id="@+id/indicator_received"
+                    android:layout_width="?attr/TextSizeInfo"
+                    android:layout_height="?attr/TextSizeInfo"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginLeft="4sp"
+                    android:alpha="0.54"
+                    android:gravity="center_vertical"
+                    android:src="@drawable/ic_received_indicator" />
+            </LinearLayout>
+        </LinearLayout>
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/message_photo"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentRight="true"
+        android:layout_marginLeft="-1.5dp"
+        android:padding="0dp"
+        android:scaleType="fitXY"
+        android:src="@drawable/ic_profile" />
+
+</RelativeLayout>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/src/main/res/values-cs/strings.xml 🔗

@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">Conversations</string>
+    <string name="action_settings">Nastavení</string>
+    <string name="action_add">Nová konverzace</string>
+    <string name="action_accounts">Nastavení účtů</string>
+    <string name="action_end_conversation">Ukončit tuto konverzaci</string>
+    <string name="action_contact_details">Detaily kontaktu</string>
+    <string name="action_muc_details">Detaily konference</string>
+    <string name="action_secure">Zabezpečená konverzace</string>
+    <string name="action_add_account">Přidat účet</string>
+    <string name="action_edit_contact">Upravit jméno</string>
+    <string name="action_add_phone_book">Přidat do telefonního seznamu</string>
+    <string name="action_delete_contact">Smazat ze seznamu</string>
+    <string name="title_activity_manage_accounts">Nastavení účtů</string>
+    <string name="title_activity_settings">Nastavení</string>
+    <string name="title_activity_conference_details">Detaily konference</string>
+    <string name="title_activity_contact_details">Detaily kontaktu</string>
+    <string name="title_activity_conversations">Konverzace</string>
+    <string name="title_activity_sharewith">Sdílet s konverzací</string>
+    <string name="title_activity_start_conversation">Začít konverzaci</string>
+    <string name="title_activity_choose_contact">Vybrat kontakt</string>
+    <string name="just_now">právě teď</string>
+    <string name="minute_ago">před 1 minutou</string>
+    <string name="minutes_ago">před %d minutami</string>
+    <string name="unread_conversations">nepřečtené konverzace</string>
+    <string name="sending">odesílám&#8230;</string>
+    <string name="encrypted_message">Dešifruji zprávu. Chvíli strpení&#8230;</string>
+    <string name="nick_in_use">Přezdívka se již používá</string>
+    <string name="admin">Administrátor</string>
+    <string name="owner">Vlastník</string>
+    <string name="moderator">Moderátor</string>
+    <string name="participant">Účastník</string>
+    <string name="visitor">Návštěvník</string>
+    <string name="remove_contact_text">Chcete odstranit %s ze svého seznamu? Konverzace spojené s tímto kontaktem nebudou odstraněny.</string>
+    <string name="remove_bookmark_text">Chcete odstranit %s ze záložek? Konverzace spojené s touto záložkou nebudou odstraněny.</string>
+    <string name="register_account">Registrovat nový účet na serveru</string>
+    <string name="share_with">Sdílet s</string>
+    <string name="start_conversation">Začít konverzaci</string>
+    <string name="invite_contact">Pozvat kontakt</string>
+    <string name="contacts">Kontakty</string>
+    <string name="cancel">Zrušit</string>
+    <string name="add">Přidat</string>
+    <string name="edit">Upravit</string>
+    <string name="delete">Smazat</string>
+    <string name="save">Uložit</string>
+    <string name="ok">OK</string>
+    <string name="crash_report_title">Aplikace Konverzace přestala reagovat</string>
+    <string name="crash_report_message">Zasláním detailů o důvodu selhání pomůžete dalšímu vývoji aplikace Konverzace\n<b>Varování:</b> Toto použije nastavený XMPP účet pro zaslání detailů vývojářům.</string>
+    <string name="send_now">Odeslat teď</string>
+    <string name="send_never">Již se neptat</string>
+    <string name="problem_connecting_to_account">Připojení k účtu se nezdařilo</string>
+    <string name="problem_connecting_to_accounts">Připojení k několika účtům se nezdařilo</string>
+    <string name="touch_to_fix">Pro nastavení účtů tapni zde</string>
+    <string name="attach_file">Přiložit soubor</string>
+    <string name="not_in_roster">Kontakt není v seznamu. Chcete ho přidat?</string>
+    <string name="add_contact">Přidat kontakt</string>
+    <string name="send_failed">doručení selhalo</string>
+    <string name="send_rejected">zamítnuto</string>
+    <string name="receiving_image">Přijímám obrázek. Chvíli strpení&#8230;</string>
+    <string name="preparing_image">Připravuji obrázek na přenos</string>
+    <string name="action_clear_history">Smazat historii</string>
+    <string name="clear_conversation_history">Smaže historii konverzací</string>
+    <string name="clear_histor_msg">Chcete smazat všechny zprávy v této konverzaci?\n\n<b>Varování:</b> Toto neovlivní zprávy uložené na jiných přístrojích nebo serverech.</string>
+    <string name="delete_messages">Smazat zprávy</string>
+    <string name="also_end_conversation">Poté ukončit i tuto konverzaci</string>
+    <string name="choose_presence">Vybrat aktualizaci stavu pro kontakt</string>
+    <string name="send_plain_text_message">Poslat textovou zprávu</string>
+    <string name="send_otr_message">Poslat OTR šifrovanou zprávu</string>
+    <string name="send_pgp_message">Poslat OpenPGP šifrovanou zprávu</string>
+    <string name="your_nick_has_been_changed">Přezdívka byla změněna</string>
+    <string name="download_image">Stáhnout obrázek</string>
+    <string name="image_offered_for_download"><i>Byl nabídnut obrázek ke stažení</i></string>
+    <string name="send_unencrypted">Poslat nešifrované</string>
+    <string name="decryption_failed">Zašifrování se nezdařilo. Možná nemáte správný privátní klíč.</string>
+    <string name="openkeychain_required">OpenKeychain</string>
+    <string name="openkeychain_required_long">Konverzace využívá aplikaci třetí strany, <b>OpenKeychain</b>, k šifrování a dešifrování zpráv a ke správě veřejných klíčů.\n\nOpenKeychain je licencován pod GPLv3 a dostupný na F-Droid a Google Play.\n\n<small>(Po instalaci prosím restartujte aplikaci Konverzace.)</small></string>
+    <string name="restart">Restartovat</string>
+    <string name="install">Instalovat</string>
+    <string name="offering">nabízí&#8230;</string>
+    <string name="waiting">čekám&#8230;</string>
+    <string name="no_pgp_key">Nebyl nalezen žádný OpenPGP klíč</string>
+    <string name="contact_has_no_pgp_key">Není možné zašifrovat zprávu v aplikaci Konverzace, protože druhá strana neoznamuje svůj veřejný klíč.\n\n<small>Požádejte svůj kontakt ať si nastaví OpenPGP.</small></string>
+    <string name="no_pgp_keys">Nebyly nalezeny žádné OpenPGP klíče</string>
+    <string name="contacts_have_no_pgp_keys">Není možné zašifrovat zprávy v aplikaci Konverzace, protože kontakty neoznamují svůj veřejný klíč.\n\n<small>Požádejte své kontakty ať si nastaví OpenPGP.</small></string>
+    <string name="encrypted_message_received"><i>Byla přijata šifrovaná zpráva. Tapni pro dešifrování a přečtení.</i></string>
+    <string name="encrypted_image_received"><i>Byl přijat šifrovaný obrázek. Tapni pro dešifrování a prohlédnutí.</i></string>
+    <string name="image_file"><i>Byl přijat obrázek. Tapni pro prohlédnutí</i></string>
+    <string name="pref_general">Obecné</string>
+    <string name="pref_xmpp_resource">XMPP zdroj</string>
+    <string name="pref_xmpp_resource_summary">Jméno se kterým se tento klient identifikuje</string>
+    <string name="pref_accept_files">Přijímat soubory</string>
+    <string name="pref_accept_files_summary">Automaticky přijímat soubory menší než&#8230;</string>
+    <string name="pref_notification_settings">Nastavení upozornění</string>
+    <string name="pref_notifications">Upozornění</string>
+    <string name="pref_notifications_summary">Upozornit při přijetí nové zprávy</string>
+    <string name="pref_vibrate">Vibrovat</string>
+    <string name="pref_vibrate_summary">Vibrovat při přijetí nové zprávy</string>
+    <string name="pref_sound">Zvuk</string>
+    <string name="pref_sound_summary">Přehrát zvuk společně s upozorněním</string>
+    <string name="pref_conference_notifications">Upozornění při konferencích</string>
+    <string name="pref_conference_notifications_summary">Vždy upozorňovat při nové konferenční zprávě, nejen pokud je vybrána</string>
+    <string name="pref_notification_grace_period">Četnost upozornění</string>
+    <string name="pref_notification_grace_period_summary">Neupozorňovat krátce poté co byla obdržena kopie zprávy</string>
+    <string name="pref_advanced_options">Pokročilé nastavení</string>
+    <string name="pref_never_send_crash">Neodesílat detaily o pádu aplikace</string>
+    <string name="pref_never_send_crash_summary">Zasláním detailů o důvodu selhání pomůžete dalšímu vývoji aplikace Konverzace</string>
+    <string name="pref_confirm_messages">Potvrzovat zprávy</string>
+    <string name="pref_confirm_messages_summary">Dá vědět kontaktům, že zpráva byla přijata a přečtena</string>
+    <string name="pref_ui_options">Nastavení UI</string>
+    <string name="openpgp_error">OpenKeychain nahlásil chybu</string>
+    <string name="error_decrypting_file">I/O chyba dešifrování souboru</string>
+    <string name="accept">Přijmout</string>
+    <string name="error">Došlo k chybě</string>
+    <string name="pref_grant_presence_updates">Povolit aktualizace stavu</string>
+    <string name="pref_grant_presence_updates_summary">Aktivně povolovat a žádat o zasílání změn stavu pro vytvářené kontakty</string>
+    <string name="subscriptions">Odběry</string>
+    <string name="your_account">Váš účet</string>
+    <string name="keys">Klíče</string>
+    <string name="send_presence_updates">Zasílat změny stavu</string>
+    <string name="receive_presence_updates">Přijímat změny stavu</string>
+    <string name="ask_for_presence_updates">Zažádat o změny stavu</string>
+    <string name="attach_choose_picture">Vybrat obrázek</string>
+    <string name="attach_take_picture">Vyfotit obrázek</string>
+    <string name="preemptively_grant">Aktivně povolovat vyžádání změnu stavu</string>
+    <string name="error_not_an_image_file">Vybraný soubor není obrázek</string>
+    <string name="error_compressing_image">Chyba při konverzi obrázkového souboru</string>
+    <string name="error_file_not_found">Soubor nenalezen</string>
+    <string name="error_io_exception">Obecná I/O chyba. Že by již nebylo volné místo?</string>
+    <string name="error_security_exception_during_image_copy">Aplikace, která byla vybrána pro výběr obrázku, nepovolila přečtení souboru.\n\n<small>Zkuste použít jiného správce souborů pro výběr obrázku</small></string>
+    <string name="account_status_unknown">Neznámý</string>
+    <string name="account_status_disabled">Dočasně vypnuto</string>
+    <string name="account_status_online">Online</string>
+    <string name="account_status_connecting">Připojuji\u2026</string>
+    <string name="account_status_offline">Offline</string>
+    <string name="account_status_unauthorized">Nepřihlášen</string>
+    <string name="account_status_not_found">Server nenalezen</string>
+    <string name="account_status_no_internet">Žádné připojení</string>
+    <string name="account_status_regis_fail">Registrace selhala</string>
+    <string name="account_status_regis_conflict">Uživatelské jméno se již používá</string>
+    <string name="account_status_regis_success">Registrace dokončena</string>
+    <string name="account_status_regis_not_sup">Server nepodporuje registrace</string>
+    <string name="encryption_choice_none">Čistý text</string>
+    <string name="encryption_choice_otr">OTR</string>
+    <string name="encryption_choice_pgp">OpenPGP</string>
+    <string name="mgmt_account_edit">Upravit účet</string>
+    <string name="mgmt_account_delete">Smazat účet</string>
+    <string name="mgmt_account_disable">Dočasně vypnout</string>
+    <string name="mgmt_account_publish_avatar">Zveřejnit avatar</string>
+    <string name="mgmt_account_publish_pgp">Zveřejnit OpenPGP klíč</string>
+    <string name="mgmt_account_enable">Povolit účet</string>
+    <string name="mgmt_account_are_you_sure">Jste si jisti?</string>
+    <string name="mgmt_account_delete_confirm_text">Pokud smažete svůj účet celá historie konverzací bude ztracena</string>
+    <string name="attach_record_voice">Nahrát hlas</string>
+    <string name="account_settings_jabber_id">Jabber ID</string>
+    <string name="account_settings_password">Heslo</string>
+    <string name="account_settings_example_jabber_id">jmeno@server.cz</string>
+    <string name="account_settings_confirm_password">Potvrdit heslo</string>
+    <string name="password">Heslo</string>
+    <string name="confirm_password">Potvrdit heslo</string>
+    <string name="passwords_do_not_match">Hesla nesouhlasí</string>
+    <string name="invalid_jid">Toto není platné Jabber ID</string>
+    <string name="error_out_of_memory">Nedostatek paměti. Obrázek je příliš velký</string>
+    <string name="add_phone_book_text">Chcete přidat %s do svého telefonního seznamu?</string>
+    <string name="contact_status_online">online</string>
+    <string name="contact_status_free_to_chat">volný pro chat</string>
+    <string name="contact_status_away">pryč</string>
+    <string name="contact_status_extended_away">rozšířené pryč</string>
+    <string name="contact_status_do_not_disturb">nerušit</string>
+    <string name="contact_status_offline">offline</string>
+    <string name="muc_details_conference">Konference</string>
+    <string name="muc_details_other_members">Ostatní členové</string>
+    <string name="server_info_carbon_messages">XEP-0280: Kopie zpráv</string>
+    <string name="server_info_stream_management">XEP-0198: Nastavení proudu</string>
+    <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+    <string name="server_info_available">dostupný</string>
+    <string name="server_info_unavailable">nedostupný</string>
+    <string name="missing_public_keys">Chybí oznámení o veřejném klíči</string>
+    <string name="last_seen_now">právě spatřen</string>
+    <string name="last_seen_min">naposledy spatřen před 1 minutou</string>
+    <string name="last_seen_mins">naposledy spatřen před %d minutami</string>
+    <string name="last_seen_hour">naposledy spatřen před 1 hodinou</string>
+    <string name="last_seen_hours">naposledy spatřen před %d hodinami</string>
+    <string name="last_seen_day">naposledy spatřen před 1 dnem</string>
+    <string name="last_seen_days">naposledy spatřen před %d dny</string>
+    <string name="never_seen">nebyl nikdy spatřen</string>
+    <string name="install_openkeychain">Šifrovaná zpráva. Nainstaluje prosím OpenKeychain pro dešifrování.</string>
+    <string name="unknown_otr_fingerprint">Neznámý OTR identifikátor</string>
+    <string name="openpgp_messages_found">Nalezena OpenPGP šifrovaná zpráva</string>
+    <string name="reception_failed">Příjem selhal</string>
+    <string name="your_fingerprint">Váš identifikátor</string>
+    <string name="otr_fingerprint">OTR identifikátor</string>
+    <string name="verify">Ověřit</string>
+    <string name="decrypt">Dešifrovat</string>
+    <string name="conferences">Konference</string>
+    <string name="search">Hledat</string>
+    <string name="create_contact">Vytvořit kontakt</string>
+    <string name="join_conference">Připojit ke konferenci</string>
+    <string name="delete_contact">Smazat kontakt</string>
+    <string name="view_contact_details">Zobrazit detaily kontaktu</string>
+    <string name="create">Vytvořit</string>
+    <string name="contact_already_exists">Kontakt již existuje</string>
+    <string name="join">Vstoupit</string>
+    <string name="conference_address">Adresa konference</string>
+    <string name="conference_address_example">mistnost@konference.server.cz</string>
+    <string name="save_as_bookmark">Uložit jako záložku</string>
+    <string name="delete_bookmark">Smazat záložku</string>
+    <string name="bookmark_already_exists">Tato záložka již existuje</string>
+    <string name="you">Já</string>
+    <string name="action_edit_subject">Upravit jméno konference</string>
+    <string name="conference_not_found">Konference nenalezena</string>
+    <string name="leave">Odejít</string>
+    <string name="contact_added_you">Kontakt přidán do seznamu</string>
+    <string name="add_back">Opět přidat</string>
+    <string name="contact_has_read_up_to_this_point">%s dočetl až sem</string>
+    <string name="publish">Zveřejnit</string>
+    <string name="touch_to_choose_picture">Tapnout na avatar a vybrat obrázek z galerie</string>
+    <string name="publish_avatar_explanation">Pozor: Každý s povolením vidět změny stavu uvidí tento obrázek.</string>
+    <string name="publishing">Zveřejňuji&#8230;</string>
+    <string name="error_publish_avatar_server_reject">Server odmítl toto zveřejnění</string>
+    <string name="error_publish_avatar_converting">Při konverzi obrázku se něco nezdařilo</string>
+    <string name="error_saving_avatar">Nepodařilo se uložit avatar na disk</string>
+    <string name="or_long_press_for_default">(Stisknout dlouze pro obnovení výchozího stavu)</string>
+    <string name="error_publish_avatar_no_server_support">Váš server nepodporuje zveřejňování avataru</string>
+    <string name="private_message">šeptem</string>
+    <string name="private_message_to">pro %s</string>
+    <string name="send_private_message_to">Zaslat soukromou zprávu pro %s</string>
+    <string name="connect">Připojit</string>
+    <string name="account_already_exists">Tentou účet již existuje</string>
+    <string name="next">Další</string>
+    <string name="server_info_session_established">Současné sezení vytvořeno</string>
+    <string name="additional_information">Dodatečné informace</string>
+    <string name="skip">Přeskočit</string>
+    <string name="disable_notifications">Vypnout upozornění</string>
+    <string name="disable_notifications_for_this_conversation">Vypnout upozornění pro tuto konverzaci</string>
+    <string name="notifications_disabled">Upozornění jsou vypnuta</string>
+    <string name="enable">Povolit</string>
+    <string name="conference_requires_password">Konference vyžaduje heslo</string>
+    <string name="enter_password">Vložit heslo</string>
+    <string name="missing_presence_updates">Kontakt nezasílá informace o změně stavu</string>
+    <string name="request_presence_updates">Nejdříve si prosím vyžádejte povolení o zasílání změn stavu kontatku.\n\n<small>To bude poté použito pro zjištění jakou aplikaci tento kontakt používá.</small></string>
+    <string name="request_now">Ihned vyžádat</string>
+    <string name="delete_fingerprint">Smazat identifikátor</string>
+    <string name="sure_delete_fingerprint">Chcete opravdu smazat tento identifikátor?</string>
+    <string name="ignore">Ignorovat</string>
+    <string name="without_mutual_presence_updates"><b>Varování:</b> Odeslání bez povolení změn stavu může způsobit nečekané problémy na obou stranách.\n\n<small>Přejdi na detaily kontaktu pro ověření povolení o změnách stavu.</small></string>
+    <string name="pref_encryption_settings">Nastavení šifrování</string>
+    <string name="pref_force_encryption">Vynutit šifrování</string>
+    <string name="pref_force_encryption_summary">Vždy zasílat šifrované zprávy (mimo konference)</string>
+    <string name="pref_dont_save_encrypted">Neukládat šifrované zprávy</string>
+    <string name="pref_dont_save_encrypted_summary">Varování: Toto může vést ke ztrátě zpráv</string>
+    <string name="pref_expert_options">Expertní nastavení</string>
+    <string name="pref_expert_options_summary">S tímto zacházejte velmi opatrně</string>
+    <string name="pref_use_larger_font">Zvětšit velikost písma</string>
+    <string name="pref_use_larger_font_summary">Použít větší písmo v celé aplikaci</string>
+    <string name="pref_use_send_button_to_indicate_status">Tlačítko pro odeslání zobrazuje stav</string>
+    <string name="pref_use_send_button_to_indicate_status_summary">Obarvit tlačítko pro odeslání barvou stavu kontaktu</string>
+
+</resources>

conversations/src/main/res/values-de/arrays.xml 🔗

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string-array name="resources">
+        <item>Mobile</item>
+        <item>Phone</item>
+        <item>Tablet</item>
+        <item>Conversations</item>
+        <item>Android</item>
+    </string-array>
+    <string-array name="filesizes">
+        <item>nie</item>
+        <item>256 KB</item>
+        <item>512 KB</item>
+        <item>1 MB</item>
+    </string-array>
+    <string-array name="filesizes_values">
+        <item>0</item>
+        <item>262144</item>
+        <item>524288</item>
+        <item>1048576</item>
+    </string-array>
+    <string-array name="mute_options_descriptions">
+        <item>30 Minuten</item>
+        <item>eine Stunde</item>
+        <item>2 Stunden</item>
+        <item>8 Stunden</item>
+        <item>bis auf Widerruf</item>
+    </string-array>
+
+</resources>

conversations/src/main/res/values-de/strings.xml 🔗

@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">Conversations</string>
+    <string name="action_settings">Einstellungen</string>
+    <string name="action_add">Neue Unterhaltung</string>
+    <string name="action_accounts">Konten verwalten</string>
+    <string name="action_end_conversation">Unterhaltung beenden</string>
+    <string name="action_contact_details">Kontaktdetails</string>
+    <string name="action_muc_details">Konferenzdetails</string>
+    <string name="action_secure">Verschlüsselte Unterhaltung</string>
+    <string name="action_add_account">Konto hinzufügen</string>
+    <string name="action_edit_contact">Name bearbeiten</string>
+    <string name="action_add_phone_book">Zum Telefonbuch hinzufügen</string>
+    <string name="action_delete_contact">Aus Kontaktliste entfernen</string>
+    <string name="title_activity_manage_accounts">Konten verwalten</string>
+    <string name="title_activity_settings">Einstellungen</string>
+    <string name="title_activity_conference_details">Konferenzdetails</string>
+    <string name="title_activity_contact_details">Kontaktdetails</string>
+    <string name="title_activity_conversations">Conversations</string>
+    <string name="title_activity_sharewith">Mit Unterhaltung teilen</string>
+    <string name="title_activity_start_conversation">Beginne Unterhaltung</string>
+    <string name="title_activity_choose_contact">Kontakt auswählen</string>
+    <string name="just_now">gerade</string>
+    <string name="minute_ago">vor einer Minute</string>
+    <string name="minutes_ago">vor %d Minuten</string>
+    <string name="unread_conversations">ungelesene Unterhaltungen</string>
+    <string name="sending">senden&#8230;</string>
+    <string name="encrypted_message">Entschlüssele Nachricht. Bitte warten&#8230;</string>
+    <string name="nick_in_use">Nickname wird bereits verwendet</string>
+    <string name="admin">Administrator</string>
+    <string name="owner">Eigentümer</string>
+    <string name="moderator">Moderator</string>
+    <string name="participant">Teilnehmer</string>
+    <string name="visitor">Besucher</string>
+    <string name="remove_contact_text">Möchtest du %s von deiner Kontaktliste entfernen? Die Unterhaltung mit diesem Kontakt wird dabei nicht entfernt.</string>
+    <string name="remove_bookmark_text">Möchtest du das Lesezeichen %s entfernen? Die Unterhaltung mit diesem Lesezeichen wird dabei nicht entfernt.</string>
+    <string name="register_account">Neues Konto auf dem Server erstellen</string>
+    <string name="share_with">Teile mit&#8230;</string>
+    <string name="start_conversation">Beginne Unterhaltung</string>
+    <string name="invite_contact">Kontakt einladen</string>
+    <string name="contacts">Kontakte</string>
+    <string name="cancel">Abbrechen</string>
+    <string name="add">Hinzufügen</string>
+    <string name="edit">Bearbeiten</string>
+    <string name="delete">Entfernen</string>
+    <string name="save">Speichern</string>
+    <string name="ok">OK</string>
+    <string name="crash_report_title">Conversations ist abgestürzt</string>
+    <string name="crash_report_message">Durch das Einsenden von Fehlerberichten hilfst du bei der stetigen Verbesserung von Conversations.\n<b>Achtung:</b> Dies wird eines deiner XMPP-Konten benutzen, um den Entwickler zu kontaktieren.</string>
+    <string name="send_now">Jetzt abschicken</string>
+    <string name="send_never">Nie mehr nachfragen</string>
+    <string name="problem_connecting_to_account">Es gibt Probleme beim Verbindungsaufbau mit einem Konto</string>
+    <string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konto</string>
+    <string name="touch_to_fix">Drücke hier, um das Konto zu verwalten</string>
+    <string name="attach_file">Datei anfügen</string>
+    <string name="not_in_roster">Der Kontakt ist nicht in deiner Kontaktliste. Möchtest du ihn hinzufügen?</string>
+    <string name="add_contact">Kontakt hinzufügen</string>
+    <string name="send_failed">Zustellung nicht erfolgreich</string>
+    <string name="send_rejected">abgelehnt</string>
+    <string name="receiving_image">Empfange Bild. Bitte warten&#8230;</string>
+    <string name="preparing_image">Bereite Bild für die Übertragung vor</string>
+    <string name="action_clear_history">Verlauf löschen</string>
+    <string name="clear_conversation_history">Unterhaltungsverlauf löschen</string>
+    <string name="clear_histor_msg">Möchtest du alle Nachrichten in dieser Unterhaltung löschen?\n\n<b>Achtung:</b> Dies beeinflusst nicht Nachrichten, die auf anderen Geräten oder Servern gespeichert sind.</string>
+    <string name="delete_messages">Nachrichten löschen</string>
+    <string name="also_end_conversation">Diese Unterhaltung danach beenden</string>
+    <string name="choose_presence">Choose presence to contact</string>
+    <string name="send_plain_text_message">Unverschlüsselt schreiben</string>
+    <string name="send_otr_message">OTR-verschlüsselt schreiben</string>
+    <string name="send_pgp_message">OpenPGP-verschlüsselt schreiben</string>
+    <string name="your_nick_has_been_changed">Dein Nickname wurde geändert</string>
+    <string name="download_image">Bild herunterladen</string>
+    <string name="image_offered_for_download"><i>Bilddatei zum Download angeboten</i></string>
+    <string name="send_unencrypted">Unverschlüsselt verschicken</string>
+    <string name="decryption_failed">Entschlüsselung fehlgeschlagen. Vielleicht hast du nicht den richtigen privaten Schlüssel.</string>
+    <string name="openkeychain_required">OpenKeychain</string>
+    <string name="openkeychain_required_long">Conversations benutzt eine Drittanwendung namens <b>OpenKeychain</b>, um Nachrichten zu ver- und entschlüsseln und um deine Schlüssel zu verwalten.\n\nOpenKeychain ist GPLv3-lizenziert und kann über F-Droid oder Google Play bezogen werden.\n\n<small>(Bitte starte Conversations danach neu.)</small></string>
+    <string name="restart">Neustarten</string>
+    <string name="install">Installieren</string>
+    <string name="offering">angeboten&#8230;</string>
+    <string name="waiting">warten&#8230;</string>
+    <string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</string>
+    <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
+    <string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string>
+    <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
+    <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie anzuzeigen und zu entschlüsseln.</i></string>
+    <string name="encrypted_image_received"><i>Verschlüsseltes Bild erhalten. Drücke hier, um es anzuzeigen und zu entschlüsseln.</i></string>
+    <string name="image_file"><i>Bild erhalten. Drücke hier, um es anzuzeigen.</i></string>
+    <string name="pref_general">Allgemein</string>
+    <string name="pref_xmpp_resource">XMPP-Ressource</string>
+    <string name="pref_xmpp_resource_summary">Der Name, mit dem sich der Client selbst identifiziert</string>
+    <string name="pref_accept_files">Dateiannahme</string>
+    <string name="pref_accept_files_summary">Dateien, die kleiner sind als &#8230;, automatisch annehmen</string>
+    <string name="pref_notification_settings">Benachrichtigungseinstellungen</string>
+    <string name="pref_notifications">Benachrichtigungen</string>
+    <string name="pref_notifications_summary">Benachrichtige mich, wenn eine neue Nachricht ankommt</string>
+    <string name="pref_vibrate">Vibrieren</string>
+    <string name="pref_vibrate_summary">Vibriere, wenn eine neue Nachricht ankommt</string>
+    <string name="pref_sound">Klingelton</string>
+    <string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string>
+    <string name="pref_conference_notifications">Konferenz-Benachrichtigungen</string>
+    <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde.</string>
+    <string name="pref_notification_grace_period">Gnadenfrist</string>
+    <string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string>
+    <string name="pref_advanced_options">Erweiterte Optionen</string>
+    <string name="pref_never_send_crash">Sende niemals Absturzberichte</string>
+    <string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string>
+    <string name="pref_confirm_messages">Lesebestätigung senden</string>
+    <string name="pref_confirm_messages_summary">Informiere deine Kontakte, wenn du eine Nachricht empfängst oder liest</string>
+    <string name="openpgp_error">Fehler mit OpenKeychain</string>
+    <string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string>
+    <string name="accept">Annehmen</string>
+    <string name="error">Ein unbekannter Fehler ist aufgetreten</string>
+    <string name="pref_grant_presence_updates">Online-Status</string>
+    <string name="pref_grant_presence_updates_summary">Erlaube Kontakten, die von dir erstellt wurden, deinen Status zu sehen und frage um Erlaubnis, ihren sehen zu dürfen</string>
+    <string name="subscriptions">Abonnements</string>
+    <string name="your_account">Dein Konto</string>
+    <string name="keys">Schlüssel</string>
+    <string name="send_presence_updates">Anwesenheitsbenachrichtigungen senden</string>
+    <string name="receive_presence_updates">Empfange Anwesenheitsbenachrichtigungen</string>
+    <string name="ask_for_presence_updates">Frage um Erlaubnis, Anwesenheitsbenachrichtigungen sehen zu dürfen</string>
+    <string name="attach_choose_picture">Foto auswählen</string>
+    <string name="attach_take_picture">Foto aufnehmen</string>
+    <string name="preemptively_grant">Erlaube Statusanfrage vorab</string>
+    <string name="error_not_an_image_file">Die ausgewählte Datei ist kein Bild</string>
+    <string name="error_compressing_image">Fehler beim Umwandeln des Bildes</string>
+    <string name="error_file_not_found">Datei nicht gefunden</string>
+    <string name="error_io_exception">Allgemeiner Fehler. Vielleicht hast du keinen Speicherplatz mehr?</string>
+    <string name="error_security_exception_during_image_copy">Die App, mit der du das Bild ausgesucht hast, hat uns keine Rechte eingeräumt, das Bild zu betrachten.\n\n<small>Benutze einen anderen Dateimanager</small></string>
+    <string name="account_status_unknown">Unbekannt</string>
+    <string name="account_status_disabled">Vorübergehend abgeschaltet</string>
+    <string name="account_status_online">Online</string>
+    <string name="account_status_connecting">Verbinde\u2026</string>
+    <string name="account_status_offline">Offline</string>
+    <string name="account_status_unauthorized">Ungültige Zugangsdaten</string>
+    <string name="account_status_not_found">Server nicht gefunden</string>
+    <string name="account_status_no_internet">Keine Internetverbindung</string>
+    <string name="account_status_regis_fail">Registrierung fehlgeschlagen</string>
+    <string name="account_status_regis_conflict">Benutzername wird bereits verwendet</string>
+    <string name="account_status_regis_success">Registrierung abgeschlossen</string>
+    <string name="account_status_regis_not_sup">Der Server unterstützt keine Registrierung</string>
+    <string name="encryption_choice_none">Klartext</string>
+    <string name="encryption_choice_otr">OTR</string>
+    <string name="encryption_choice_pgp">OpenPGP</string>
+    <string name="mgmt_account_edit">Konto bearbeiten</string>
+    <string name="mgmt_account_delete">Löschen</string>
+    <string name="mgmt_account_disable">Vorübergehend abschalten</string>
+    <string name="mgmt_account_publish_avatar">Avatar veröffentlichen</string>
+    <string name="mgmt_account_publish_pgp">Öffentlichen OpenPGP-Schlüssel veröffentlichen</string>
+    <string name="mgmt_account_enable">Anschalten</string>
+    <string name="mgmt_account_are_you_sure">Bist du dir sicher?</string>
+    <string name="mgmt_account_delete_confirm_text">Wenn du dein Konto löscht, gehen alle Gesprächsverläufe verloren</string>
+    <string name="attach_record_voice">Sprache aufzeichnen</string>
+    <string name="account_settings_jabber_id">Jabber-ID:</string>
+    <string name="account_settings_password">Passwort:</string>
+    <string name="account_settings_example_jabber_id">benutzer@domain.de</string>
+    <string name="account_settings_confirm_password">Passwort bestätigen</string>
+    <string name="password">Passwort</string>
+    <string name="confirm_password">Passwort bestätigen</string>
+    <string name="passwords_do_not_match">Passwörter stimmen nicht überein</string>
+    <string name="invalid_jid">Ungültige Jabber-ID</string>
+    <string name="error_out_of_memory">Zu wenig Speicher vorhanden. Das Bild ist zu groß</string>
+    <string name="add_phone_book_text">Möchtest du %s zum Telefonbuch hinzufügen?</string>
+    <string name="contact_status_online">Online</string>
+    <string name="contact_status_free_to_chat">Bereit</string>
+    <string name="contact_status_away">Abwesend</string>
+    <string name="contact_status_extended_away">Abwesend (erweitert)</string>
+    <string name="contact_status_do_not_disturb">Nicht stören</string>
+    <string name="contact_status_offline">Offline</string>
+    <string name="muc_details_conference">Konferenz</string>
+    <string name="muc_details_other_members">Andere Mitglieder</string>
+    <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+    <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+    <string name="server_info_pep">XEP-0163: PEP (Avatare)</string>
+    <string name="server_info_available">verfügbar</string>
+    <string name="server_info_unavailable">nicht verfügbar</string>
+    <string name="missing_public_keys">Öffentlicher Schlüssel fehlt</string>
+    <string name="last_seen_now">Gerade online</string>
+    <string name="last_seen_min">Vor einer Minute gesehen</string>
+    <string name="last_seen_mins">Vor %d Minuten gesehen</string>
+    <string name="last_seen_hour">Vor einer Stunde gesehen</string>
+    <string name="last_seen_hours">Vor %d Stunden gesehen</string>
+    <string name="last_seen_day">Vor einem Tag gesehen</string>
+    <string name="last_seen_days">Vor %d Tagen gesehen</string>
+    <string name="never_seen">Noch nie gesehen</string>
+    <string name="install_openkeychain">Verschlüsselte Nachricht. Bitte installiere OpenKeychain zur Entschlüsselung.</string>
+    <string name="unknown_otr_fingerprint">Unbekannter OTR-Fingerabdruck</string>
+    <string name="openpgp_messages_found">Verschlüsselte OpenPGP-Nachricht gefunden</string>
+    <string name="reception_failed">Empfang ist fehlgeschlagen</string>
+    <string name="your_fingerprint">Dein Fingerabdruck</string>
+    <string name="otr_fingerprint">OTR-Fingerabdruck</string>
+    <string name="verify">Verifizieren</string>
+    <string name="decrypt">Entschlüsseln</string>
+    <string name="conferences">Konferenzen</string>
+    <string name="search">Suche</string>
+    <string name="create_contact">Kontakt erstellen</string>
+    <string name="join_conference">Konferenz beitreten</string>
+    <string name="delete_contact">Kontakt löschen</string>
+    <string name="view_contact_details">Kontaktdetails anzeigen</string>
+    <string name="create">Erstellen</string>
+    <string name="contact_already_exists">Der Kontakt existiert bereits</string>
+    <string name="join">Beitreten</string>
+    <string name="conference_address">Konferenzadresse</string>
+    <string name="conference_address_example">raum@conference.example.com</string>
+    <string name="save_as_bookmark">Als Lesezeichen speichern</string>
+    <string name="delete_bookmark">Lesezeichen löschen</string>
+    <string name="bookmark_already_exists">Das Lesezeichen existiert bereits</string>
+    <string name="you">Du</string>
+    <string name="action_edit_subject">Konferenzthema anpassen</string>
+    <string name="conference_not_found">Konferenz nicht gefunden</string>
+    <string name="leave">Verlassen</string>
+    <string name="contact_added_you">Der Kontakt hat dich zur Kontaktliste hinzugefügt</string>
+    <string name="add_back">Auch hinzufügen</string>
+    <string name="contact_has_read_up_to_this_point">%s hat bis zu diesem Punkt gelesen</string>
+    <string name="publish">Veröffentlichen</string>
+    <string name="touch_to_choose_picture">Klicke hier, um einen Avatar auszuwählen</string>
+    <string name="publish_avatar_explanation">Achtung: Jeder, der deinen Status sehen darf, sieht auch deinen Avatar.</string>
+    <string name="publishing">Veröffentliche&#8230;</string>
+    <string name="error_publish_avatar_server_reject">Der Server hat die Veröffentlichung des Avatars abgelehnt.</string>
+    <string name="error_publish_avatar_converting">Bei der Konvertierung des Avatars lief etwas schief.</string>
+    <string name="error_saving_avatar">Kann Avatar nicht speichern.</string>
+    <string name="or_long_press_for_default">(Oder klicke lange, um Standard wiederherzustellen)</string>
+    <string name="error_publish_avatar_no_server_support">Dein Server unterstützt die Veröffentlichung von Avataren nicht.</string>
+    <string name="private_message">private Nachricht</string>
+    <string name="private_message_to">an %s</string>
+    <string name="send_private_message_to">Sende private Nachricht an %s</string>
+    <string name="connect">Verbinden</string>
+    <string name="account_already_exists">Das Konto existiert bereits</string>
+    <string name="next">Weiter</string>
+    <string name="server_info_session_established">Aktuelle Sitzung wiederhergestellt</string>
+    <string name="additional_information">Zusätzliche Informationen</string>
+    <string name="skip">Überspringen</string>
+    <string name="pref_ui_options">Benutzeroberfläche</string>
+    <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string>
+    <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte das dies nicht unbedingt in allen Fällen funktioniert.</string>
+    <string name="disable_notifications">Benachrichtigungen deaktivieren</string>
+    <string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string>
+    <string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string>
+    <string name="enable">Aktivieren</string>
+    <string name="conference_requires_password">Konferenz ist passwortgeschützt</string>
+    <string name="enter_password">Passwort eingeben</string>
+    <string name="missing_presence_updates">Fehlender Online-Status vom Kontakt</string>
+    <string name="request_presence_updates">Bitte erst Anwesenheitsbenachrichtigungen vom Kontakt anfordern.\n\n</string>
+    <string name="request_now">Jetzt anfordern</string>
+    <string name="delete_fingerprint">Fingerabdruck löschen</string>
+    <string name="sure_delete_fingerprint">Soll dieser Fingerabdruck definitiv gelöscht werden?</string>
+    <string name="ignore">Ignorieren</string>
+    <string name="without_mutual_presence_updates"><b>Achtung:</b> Es kann zu unerwarteten Problemen führen, dies ohne gegenseitige Anwesenheitsbenachrichtigungen abzusenden.\n\n<small>Bitte die Online-Status-Abonnements in den Kontaktdetails prüfen.</small></string>
+    <string name="pref_encryption_settings">Verschlüsselungs-Einstellungen</string>
+    <string name="pref_force_encryption">Ende-zu-Ende-Verschlüsselung forcieren</string>
+    <string name="pref_force_encryption_summary">Nachrichten immer verschlüsseln (außer für Konferenzen)</string>
+    <string name="pref_dont_save_encrypted">Verschlüsselte Nachrichten nicht speichern</string>
+    <string name="pref_dont_save_encrypted_summary">Achtung: Kann zu Nachrichtenverlust führen</string>
+    <string name="pref_expert_options">Einstellungen für Experten</string>
+    <string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string>
+    <string name="pref_use_larger_font">Schriftgröße erhöhen</string>
+    <string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string>
+    <string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string>
+    <string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string>
+    <string name="pref_expert_options_other">Sonstiges</string>
+    <string name="pref_conference_name">Konferenz-Name</string>
+    <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Name verwenden</string>
+    <string name="toast_message_otr_fingerprint">OTR Fingerabdruck in die Zwischenablage kopiert!</string>
+    <string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string>
+    <string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string>
+    <string name="conference_kicked">Du wurdest aus dem Konferenzraum geworfen</string>
+
+</resources>

conversations/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>

conversations/src/main/res/values-es/strings.xml 🔗

@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">Conversations</string>
+    <string name="action_settings">Ajustes</string>
+    <string name="action_add">Nueva conversación</string>
+    <string name="action_accounts">Gestionar cuentas</string>
+    <string name="action_end_conversation">Terminar conversación</string>
+    <string name="action_contact_details">Detalles del contacto</string>
+    <string name="action_muc_details">Detalles de la conferencia</string>
+    <string name="action_secure">Conversación segura</string>
+    <string name="action_add_account">Añadir cuenta</string>
+    <string name="action_edit_contact">Editar contacto</string>
+    <string name="action_delete_contact">Eliminar contacto de la lista</string>
+    <string name="action_add_phone_book">Añadir a contactos del teléfono</string>
+    <string name="title_activity_manage_accounts">Gestionar Cuentas</string>
+    <string name="title_activity_settings">Ajustes</string>
+    <string name="title_activity_conference_details">Detalles de Conferencia</string>
+    <string name="title_activity_contact_details">Detalles del Contacto</string>
+    <string name="title_activity_conversations">Conversations</string>
+    <string name="title_activity_sharewith">Compartir con Conversación</string>
+    <string name="title_activity_start_conversation">Nueva Conversación</string>
+    <string name="title_activity_choose_contact">Elegir Contacto</string>
+    <string name="just_now">ahora</string>
+    <string name="minute_ago">hace 1 min</string>
+    <string name="minutes_ago">hace %d min</string>
+    <string name="unread_conversations">conversaciones por leer</string>
+    <string name="sending">enviando&#8230;</string>
+    <string name="encrypted_message">Desencriptando mensaje. Espera por favor&#8230;</string>
+    <string name="nick_in_use">El apodo ya está en uso</string>
+    <string name="admin">Administrador</string>
+    <string name="owner">Propietario</string>
+    <string name="moderator">Moderador</string>
+    <string name="participant">Participante</string>
+    <string name="visitor">Visitante</string>
+    <string name="remove_contact_text">¿Quieres eliminar a %s de tu lista? La conversación asociada a esta cuenta no se eliminará.</string>
+    <string name="remove_bookmark_text">¿Quieres eliminar %s de tus marcadores? La conversación de la conferencia asociada con este marcador no se eliminará.</string>
+    <string name="register_account">Registrar nueva cuenta en servidor</string>
+    <string name="share_with">Compartir con</string>
+    <string name="start_conversation">Comenzar conversación</string>
+    <string name="invite_contact">Invitar contactos</string>
+    <string name="contacts">Contactos</string>
+    <string name="cancel">Cancelar</string>
+    <string name="add">Añadir</string>
+    <string name="edit">Editar</string>
+    <string name="delete">Eliminar</string>
+    <string name="save">Guardar</string>
+    <string name="ok">OK</string>
+    <string name="crash_report_title">Conversations se ha detenido.</string>
+    <string name="crash_report_message">Si envías un informe de fallos ayudas al desarrollo de Conversations\n<b>Aviso:</b> Esto usará tu cuenta XMPP para enviar los registros de error al desarrollador.</string>
+    <string name="send_now">Enviar ahora</string>
+    <string name="send_never">No preguntar de nuevo</string>
+    <string name="problem_connecting_to_account">No se ha podido conectar a la cuenta</string>
+    <string name="problem_connecting_to_accounts">No se ha podido conectar a múltiples cuentas</string>
+    <string name="touch_to_fix">Pulsa aquí para gestionar tus cuentas</string>
+    <string name="attach_file">Adjuntar</string>
+    <string name="not_in_roster">El contacto no está en tu lista. ¿Te gustaría añadirlo?</string>
+    <string name="add_contact">Añadir contacto</string>
+    <string name="send_failed">Error al enviar</string>
+    <string name="send_rejected">rechazado</string>
+    <string name="receiving_image">Recibiendo archivo de imagen. Espera por favor&#8230;</string>
+    <string name="preparing_image">Preparando imagen para enviar</string>
+    <string name="action_clear_history">Limpiar historial</string>
+    <string name="clear_conversation_history">Limpiar historial de conversación</string>
+    <string name="clear_histor_msg">¿Quieres borrar todos los mensajes de esta conversación?\n\n<b>Aviso:</b> Esto no afectará a los mensajes guardados en otros dispositivos o servidores.</string>
+    <string name="delete_messages">Borrar mensajes</string>
+    <string name="also_end_conversation">Terminar esta conversación más tarde</string>
+    <string name="choose_presence">Selecciona recurso del contacto</string>
+    <string name="send_plain_text_message">Enviar mensaje de texto</string>
+    <string name="send_otr_message">Enviar mensaje encriptado con OTR</string>
+    <string name="send_pgp_message">Enviar mensaje encriptado con OpenPGP</string>
+    <string name="your_nick_has_been_changed">Tu apodo se ha modificado</string>
+    <string name="download_image">Descargar imagen</string>
+    <string name="image_offered_for_download"><i>Archivo de imagen ofrecido para descarga</i></string>
+    <string name="send_unencrypted">Enviar sin encriptar</string>
+    <string name="decryption_failed">Falló la desencriptación. Tal vez no tengas la clave privada apropiada.</string>
+    <string name="openkeychain_required">OpenKeychain</string>
+    <string name="openkeychain_required_long">Conversations utiliza una aplicación de terceros llamada <b>OpenKeychain</b> para encriptar y desencriptar mensajes y gestionar tus claves públicas.\n\nOpenKeychain está publicado bajo licencia GPLv3 y disponible on F-Droid y Google Play.\n\n<small>(Por favor, reinicie Conversations después.)</small></string>
+    <string name="restart">Reiniciar</string>
+    <string name="install">Instalar</string>
+    <string name="offering">ofreciendo&#8230;</string>
+    <string name="waiting">esperando&#8230;</string>
+    <string name="no_pgp_key">Clave OpenPGP no encontrada</string>
+    <string name="contact_has_no_pgp_key">Conversations no ha podido encriptar tus mensajes porque el contacto no está anunciando su clave publica.\n\n<small>Por favor, pide a tu contacto que configure OpenPGP.</small></string>
+    <string name="no_pgp_keys">Claves OpenPGP no encontradas</string>
+    <string name="contacts_have_no_pgp_keys">Conversations no ha podido encriptar tus mensajes porque tus contactos no están anunciando su clave publica.\n\n<small>Por favor, pide a tus contactos que configuren OpenPGP.</small></string>
+    <string name="encrypted_message_received"><i>Mensaje encriptado recibido. Pulsa para ver.</i></string>
+    <string name="encrypted_image_received"><i>Imagen encriptada recibida. Pulsa para ver.</i></string>
+    <string name="image_file"><i>Imagen recibida. Pulsa para ver</i></string>
+    <string name="pref_general">General</string>
+    <string name="pref_xmpp_resource">Recurso</string>
+    <string name="pref_xmpp_resource_summary">El nombre que identifica el cliente que estás utilizando</string>
+    <string name="pref_accept_files">Aceptar archivos</string>
+    <string name="pref_accept_files_summary">De forma automática aceptar archivos menores que&#8230;</string>
+    <string name="pref_notification_settings">Ajustes de notificación</string>
+    <string name="pref_notifications">Notificaciones</string>
+    <string name="pref_notifications_summary">Notifica cuando llega un nuevo mensaje</string>
+    <string name="pref_vibrate">Vibrar</string>
+    <string name="pref_vibrate_summary">Vibra cuando llega un nuevo mensaje</string>
+    <string name="pref_sound">Sonido</string>
+    <string name="pref_sound_summary">Reproduce tono con la notificación</string>
+    <string name="pref_conference_notifications">Notificaciones de conferencia</string>
+    <string name="pref_conference_notifications_summary">Siempre notifica cuando llega un mensaje de conferencia y no solo cuando llega un mensaje destacado</string>
+    <string name="pref_notification_grace_period">Notificaciones Carbons</string>
+    <string name="pref_notification_grace_period_summary">Deshabilita las notificaciones durante un corto periodo de tiempo después de recibir la copia del mensaje carbon</string>
+    <string name="pref_advanced_options">Opciones avanzadas</string>
+    <string name="pref_never_send_crash">Nunca enviar informe de fallos</string>
+    <string name="pref_never_send_crash_summary">Si envías registros de error ayudas al desarrollo de Conversations</string>
+    <string name="pref_confirm_messages">Confirmar Mensajes</string>
+    <string name="pref_confirm_messages_summary">Permitir a tus contactos saber cuando recibes y lees un mensaje</string>
+    <string name="pref_ui_options">Opciones de interfaz</string>
+    <string name="openpgp_error">OpenKeychain reportó un error</string>
+    <string name="error_decrypting_file">Error desencriptando fichero</string>
+    <string name="accept">Aceptar</string>
+    <string name="error">Ha ocurrido un error</string>
+    <string name="pref_grant_presence_updates">Suscripción de presencia</string>
+    <string name="pref_grant_presence_updates_summary">De forma automática solicitar y conceder suscripciones de presencia de los contactos que has creado</string>
+    <string name="subscriptions">Suscripciones</string>
+    <string name="your_account">Tu cuenta</string>
+    <string name="keys">Claves</string>
+    <string name="send_presence_updates">Enviar actualizaciones de presencia</string>
+    <string name="receive_presence_updates">Recibir actualizaciones de presencia</string>
+    <string name="ask_for_presence_updates">Solicitar actualizaciones de presencia</string>
+    <string name="attach_choose_picture">Seleccionar imagen</string>
+    <string name="attach_take_picture">Hacer foto</string>
+    <string name="preemptively_grant">De forma automática conceder solicitud de suscripción</string>
+    <string name="error_not_an_image_file">El archivo seleccionado no es una imagen</string>
+    <string name="error_compressing_image">Error comprimiendo el archivo de imagen</string>
+    <string name="error_file_not_found">Archivo no encontrado</string>
+    <string name="error_io_exception">Error general. ¿Puede que no tengas espacio en disco?</string>
+    <string name="error_security_exception_during_image_copy">La aplicación que usas para seleccionar imágenes no proporciona suficientes permisos para leer el archivo.\n\n<small>Utiliza un explorador de ficheros diferente para seleccionar la imagen</small></string>
+    <string name="account_status_unknown">Desconocido</string>
+    <string name="account_status_disabled">Deshabilitado temporalmente</string>
+    <string name="account_status_online">Conectado</string>
+    <string name="account_status_connecting">Conectando\u2026</string>
+    <string name="account_status_offline">Desconectado</string>
+    <string name="account_status_unauthorized">No autorizado</string>
+    <string name="account_status_not_found">Servidor no encontrado</string>
+    <string name="account_status_no_internet">Sin conectividad</string>
+    <string name="account_status_regis_fail">Error en el registro</string>
+    <string name="account_status_regis_conflict">El identificador ya está en uso</string>
+    <string name="account_status_regis_success">Registro completado</string>
+    <string name="account_status_regis_not_sup">El servidor no soporta registros</string>
+    <string name="encryption_choice_none">Texto plano</string>
+    <string name="encryption_choice_otr">OTR</string>
+    <string name="encryption_choice_pgp">OpenPGP</string>
+    <string name="mgmt_account_edit">Editar cuenta</string>
+    <string name="mgmt_account_delete">Eliminar cuenta</string>
+    <string name="mgmt_account_disable">Deshabilitar temporalmente</string>
+    <string name="mgmt_account_publish_avatar">Imagen de perfil</string>
+    <string name="mgmt_account_publish_pgp">Publicar clave pública OpenPGP</string>
+    <string name="mgmt_account_enable">Habilitar</string>
+    <string name="mgmt_account_are_you_sure">¿Estás seguro?</string>
+    <string name="mgmt_account_delete_confirm_text">Si eliminas tu cuenta tu historial completo de conversaciones se perderá</string>
+    <string name="attach_record_voice">Grabar audio</string>
+    <string name="account_settings_jabber_id">Identificador Jabber</string>
+    <string name="account_settings_password">Contraseña</string>
+    <string name="account_settings_example_jabber_id">usuario@ejemplo.com</string>
+    <string name="account_settings_confirm_password">Confirmar contraseña</string>
+    <string name="password">Contraseña</string>
+    <string name="confirm_password">Confirmar contraseña</string>
+    <string name="passwords_do_not_match">Las contraseñas no coinciden</string>
+    <string name="invalid_jid">El identificador no es un identificador de Jabber válido</string>
+    <string name="error_out_of_memory">Sin memoria. La imagen es demasiado grande</string>
+    <string name="add_phone_book_text">¿Te gustaría añadir a %s a tus contactos del teléfono?</string>
+    <string name="contact_status_online">Disponible</string>
+    <string name="contact_status_free_to_chat">Hablador</string>
+    <string name="contact_status_away">Ausente</string>
+    <string name="contact_status_extended_away">Ausencia extendida</string>
+    <string name="contact_status_do_not_disturb">No molestar</string>
+    <string name="contact_status_offline">Desconectado</string>
+    <string name="muc_details_conference">Conferencia</string>
+    <string name="muc_details_other_members">Otros Miembros</string>
+    <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+    <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+    <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+    <string name="server_info_available">Sí</string>
+    <string name="server_info_unavailable">No</string>
+    <string name="missing_public_keys">Se han perdido las claves de anuncio públicas</string>
+    <string name="last_seen_now">Visto última vez ahora</string>
+    <string name="last_seen_min">Visto última vez hace 1 minuto</string>
+    <string name="last_seen_mins">Visto última vez hace %d minutos</string>
+    <string name="last_seen_hour">Visto última vez hace 1 hora</string>
+    <string name="last_seen_hours">Visto última vez hace %d horas</string>
+    <string name="last_seen_day">Visto última vez hace 1 día</string>
+    <string name="last_seen_days">Visto última vez hace %d días</string>
+    <string name="never_seen">Nunca visto</string>
+    <string name="install_openkeychain">Mensaje encriptado. Por favor instala OpenKeychain para desencriptar.</string>
+    <string name="unknown_otr_fingerprint">Clave OTR desconocida</string>
+    <string name="openpgp_messages_found">Encontrado mensaje encriptado con OpenPGP</string>
+    <string name="reception_failed">Error al recibir</string>
+    <string name="your_fingerprint">Tu clave</string>
+    <string name="otr_fingerprint">Clave OTR</string>
+    <string name="verify">Verificar</string>
+    <string name="decrypt">Desencriptar</string>
+    <string name="conferences">Conferencias</string>
+    <string name="search">Buscar</string>
+    <string name="create_contact">Crear Contacto</string>
+    <string name="join_conference">Unirse a Conferencia</string>
+    <string name="delete_contact">Eliminar Contacto</string>
+    <string name="view_contact_details">Ver detalles del contacto</string>
+    <string name="create">Crear</string>
+    <string name="contact_already_exists">El contacto ya existe</string>
+    <string name="join">Unirse</string>
+    <string name="conference_address">Dirección de la Conferencia</string>
+    <string name="conference_address_example">nombre@conferencia.ejemplo.com</string>
+    <string name="save_as_bookmark">Guardar en marcadores</string>
+    <string name="delete_bookmark">Eliminar marcador</string>
+    <string name="bookmark_already_exists">Este marcador ya exsite</string>
+    <string name="you">Tú</string>
+    <string name="action_edit_subject">Editar asunto de la conferencia</string>
+    <string name="conference_not_found">Conferencia no encontrada</string>
+    <string name="leave">Salir</string>
+    <string name="contact_added_you">El contacto te ha añadido a su lista de contactos</string>
+    <string name="add_back">Añadir contacto</string>
+    <string name="contact_has_read_up_to_this_point">%s ha leído hasta aquí</string>
+    <string name="publish">Publicar</string>
+    <string name="touch_to_choose_picture">Pulsa para seleccionar una imagen de la galería</string>
+    <string name="publish_avatar_explanation">Nota: Todos tus contactos podrán ver esta imagen.</string>
+    <string name="publishing">Publicando&#8230;</string>
+    <string name="error_publish_avatar_server_reject">El servidor rechazó la publicación</string>
+    <string name="error_publish_avatar_converting">Se ha producido un error mientras se convertía la imagen</string>
+    <string name="error_saving_avatar">No se ha podido guardar la imagen de perfil en disco</string>
+    <string name="or_long_press_for_default">(O pulsación prolongada para volver a tu imagen de la agenda)</string>
+    <string name="error_publish_avatar_no_server_support">Tu servidor no soporta la publicación de imágenes de perfil</string>
+    <string name="private_message">en privado</string>
+    <string name="private_message_to">en privado para %s</string>
+    <string name="send_private_message_to">Enviar mensaje privado a %s</string>
+    <string name="connect">Conectar</string>
+    <string name="account_already_exists">Esta cuenta ya existe</string>
+    <string name="next">Siguiente</string>
+    <string name="server_info_session_established">Inicio sesión actual</string>
+    <string name="additional_information">Información adicional</string>
+    <string name="skip">Omitir</string>
+    <string name="disable_notifications">Deshabilitar notificaciones</string>
+    <string name="disable_notifications_for_this_conversation">Deshabilitar notificaciones para esta conversación</string>
+    <string name="notifications_disabled">Las notificaciones están deshabilitadas</string>
+    <string name="enable">Habilitar</string>
+    <string name="conference_requires_password">La conferencia requiere contraseña</string>
+    <string name="enter_password">Introduce la contraseña</string>
+    <string name="missing_presence_updates">Suscripción de actualizaciones de presencia del contacto perdida</string>
+    <string name="request_presence_updates">Por favor, solicita la suscripción de presencia a tu contacto primero.\n\n<small>Esto será usado para determinar qué cliente(s) está usando tu contacto.</small></string>
+    <string name="request_now">Solicitar ahora</string>
+    <string name="delete_fingerprint">Eliminar Clave OTR</string>
+    <string name="sure_delete_fingerprint">¿Estás seguro que quieres eliminar la clave OTR?</string>
+    <string name="ignore">Ignorar</string>
+    <string name="without_mutual_presence_updates"><b>Aviso:</b> Enviando esto sin suscripción de presencia por ambas partes podría causar problemas inesperados.\n\n<small>Verficia la suscripción de presencia en detalles del contacto.</small></string>
+    <string name="pref_encryption_settings">Ajustes de encriptación</string>
+    <string name="pref_force_encryption">Forzar encriptación end-to-end</string>
+    <string name="pref_force_encryption_summary">Siempre enviar mensajes encriptados (excepto para conferencias)</string>
+    <string name="pref_dont_save_encrypted">No guardar mensajes encriptados</string>
+    <string name="pref_dont_save_encrypted_summary">Aviso: Esto podría llevar a pérdida de mensajes</string>
+    <string name="pref_expert_options">Ajustes avanzados</string>
+    <string name="pref_expert_options_summary">Por favor, cuidado con estas opciones</string>
+    <string name="pref_use_larger_font">Incrementar tamaño de fuente</string>
+    <string name="pref_use_larger_font_summary">Usar fuentes grandes en toda la aplicación</string>
+    <string name="pref_use_send_button_to_indicate_status">Botón enviar indica estado</string>
+    <string name="pref_use_indicate_received">Solicitar entrega de mensaje</string>
+    <string name="pref_use_indicate_received_summary">Cuando el contacto reciba el mensaje será indicado con una marca verde. Cuidado, esto podría no funcionar en todos los casos.</string>
+    <string name="pref_use_send_button_to_indicate_status_summary">El color del botón enviar indica el estado del contacto</string>
+    <string name="pref_expert_options_other">Otros</string>
+    <string name="pref_conference_name">Nombre de conferencia</string>
+    <string name="pref_conference_name_summary">Usar el asunto de la conferencia en lugar del identificador jabber como nombre de conferencia</string>
+    <string name="toast_message_otr_fingerprint">¡Clave OTR copiada en el portapapeles!</string>
+    <string name="conference_banned">Tu entrada a esta conferencia ha sido prohibida</string>
+    <string name="conference_members_only">Esta conferencia es solo para miembros</string>
+    <string name="conference_kicked">Has sido expulsado de esta conferencia</string>
+
+</resources>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/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>

conversations/src/main/res/values/arrays.xml 🔗

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string-array name="resources">
+        <item>Mobile</item>
+        <item>Phone</item>
+        <item>Tablet</item>
+        <item>Conversations</item>
+        <item>Android</item>
+    </string-array>
+    <string-array name="filesizes">
+        <item>never</item>
+        <item>256 KB</item>
+        <item>512 KB</item>
+        <item>1 MB</item>
+    </string-array>
+    <string-array name="filesizes_values">
+        <item>0</item>
+        <item>262144</item>
+        <item>524288</item>
+        <item>1048576</item>
+    </string-array>
+    <string-array name="mute_options_descriptions">
+        <item>30 minutes</item>
+        <item>one hour</item>
+        <item>2 hours</item>
+        <item>8 hours</item>
+        <item>until further notice</item>
+    </string-array>
+
+    <integer-array name="mute_options_durations">
+        <item>1800</item>
+        <item>3600</item>
+        <item>7200</item>
+        <item>28800</item>
+        <item>-1</item>
+    </integer-array>
+
+</resources>

conversations/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>

conversations/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>

conversations/src/main/res/values/strings.xml 🔗

@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">Conversations</string>
+    <string name="action_settings">Settings</string>
+    <string name="action_add">New conversation</string>
+    <string name="action_accounts">Manage accounts</string>
+    <string name="action_end_conversation">End this conversation</string>
+    <string name="action_contact_details">Contact details</string>
+    <string name="action_muc_details">Conference details</string>
+    <string name="action_secure">Secure conversation</string>
+    <string name="action_add_account">Add account</string>
+    <string name="action_edit_contact">Edit name</string>
+    <string name="action_add_phone_book">Add to phone book</string>
+    <string name="action_delete_contact">Delete from roster</string>
+    <string name="title_activity_manage_accounts">Manage Accounts</string>
+    <string name="title_activity_settings">Settings</string>
+    <string name="title_activity_conference_details">Conference Details</string>
+    <string name="title_activity_contact_details">Contact Details</string>
+    <string name="title_activity_conversations">Conversations</string>
+    <string name="title_activity_sharewith">Share with Conversation</string>
+    <string name="title_activity_start_conversation">Start Conversation</string>
+    <string name="title_activity_choose_contact">Choose contact</string>
+    <string name="just_now">just now</string>
+    <string name="minute_ago">1 min ago</string>
+    <string name="minutes_ago">%d mins ago</string>
+    <string name="unread_conversations">unread Conversations</string>
+    <string name="sending">sending&#8230;</string>
+    <string name="encrypted_message">Decrypting message. Please wait&#8230;</string>
+    <string name="nick_in_use">Nickname is already in use</string>
+    <string name="admin">Admin</string>
+    <string name="owner">Owner</string>
+    <string name="moderator">Moderator</string>
+    <string name="participant">Participant</string>
+    <string name="visitor">Visitor</string>
+    <string name="remove_contact_text">Would you like to remove %s from your roster? The conversation associated with this contact will not be removed.</string>
+    <string name="remove_bookmark_text">Would you like to remove %s as a bookmark? The conversation associated with this bookmark will not be removed.</string>
+    <string name="register_account">Register new account on server</string>
+    <string name="share_with">Share with</string>
+    <string name="start_conversation">Start Conversation</string>
+    <string name="invite_contact">Invite Contact</string>
+    <string name="contacts">Contacts</string>
+    <string name="cancel">Cancel</string>
+    <string name="add">Add</string>
+    <string name="edit">Edit</string>
+    <string name="delete">Delete</string>
+    <string name="save">Save</string>
+    <string name="ok">OK</string>
+    <string name="crash_report_title">Conversations has crashed</string>
+    <string name="crash_report_message">By sending in stack traces you are helping the ongoing development of Conversations\n<b>Warning:</b> This will use your XMPP account to send the stack trace to the developer.</string>
+    <string name="send_now">Send now</string>
+    <string name="send_never">Never ask again</string>
+    <string name="problem_connecting_to_account">Unable to connect to account</string>
+    <string name="problem_connecting_to_accounts">Unable to connect to multiple accounts</string>
+    <string name="touch_to_fix">Touch here to manage your accounts</string>
+    <string name="attach_file">Attach file</string>
+    <string name="not_in_roster">The contact is not in your roster. Would you like to add it?</string>
+    <string name="add_contact">Add contact</string>
+    <string name="send_failed">delivery failed</string>
+    <string name="send_rejected">rejected</string>
+    <string name="receiving_image">Receiving image file. Please wait&#8230;</string>
+    <string name="preparing_image">Preparing image for transmission</string>
+    <string name="action_clear_history">Clear history</string>
+    <string name="clear_conversation_history">Clear Conversation History</string>
+    <string name="clear_histor_msg">Do you want to delete all messages within this Conversation?\n\n<b>Warning:</b> This will not influence messages stored on other devices or servers.</string>
+    <string name="delete_messages">Delete messages</string>
+    <string name="also_end_conversation">End this conversations afterwards</string>
+    <string name="choose_presence">Choose presence to contact</string>
+    <string name="send_plain_text_message">Send plain text message</string>
+    <string name="send_otr_message">Send OTR encrypted message</string>
+    <string name="send_pgp_message">Send OpenPGP encrypted message</string>
+    <string name="your_nick_has_been_changed">Your nickname has been changed</string>
+    <string name="download_image">Download Image</string>
+    <string name="image_offered_for_download"><i>Image file offered for download</i></string>
+    <string name="send_unencrypted">Send unencrypted</string>
+    <string name="decryption_failed">Decryption failed. Maybe you don’t have the proper private key.</string>
+    <string name="openkeychain_required">OpenKeychain</string>
+    <string name="openkeychain_required_long">Conversations utilizes a third party app called <b>OpenKeychain</b> to encrypt and decrypt messages and to manage your public keys.\n\nOpenKeychain is licensed under GPLv3 and available on F-Droid and Google Play.\n\n<small>(Please restart Conversations afterwards.)</small></string>
+    <string name="restart">Restart</string>
+    <string name="install">Install</string>
+    <string name="offering">offering&#8230;</string>
+    <string name="waiting">waiting&#8230;</string>
+    <string name="no_pgp_key">No OpenPGP Key found</string>
+    <string name="contact_has_no_pgp_key">Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\n<small>Please ask your contact to setup OpenPGP.</small></string>
+    <string name="no_pgp_keys">No OpenPGP Keys found</string>
+    <string name="contacts_have_no_pgp_keys">Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\n<small>Please ask your contacts to setup OpenPGP.</small></string>
+    <string name="encrypted_message_received"><i>Encrypted message received. Touch to view and decrypt.</i></string>
+    <string name="encrypted_image_received"><i>Encrypted image received. Touch to view and decrypt.</i></string>
+    <string name="image_file"><i>Image received. Touch to view</i></string>
+    <string name="pref_general">General</string>
+    <string name="pref_xmpp_resource">XMPP resource</string>
+    <string name="pref_xmpp_resource_summary">The name this client identifies itself with</string>
+    <string name="pref_accept_files">Accept files</string>
+    <string name="pref_accept_files_summary">Automatically accept files smaller than&#8230;</string>
+    <string name="pref_notification_settings">Notification Settings</string>
+    <string name="pref_notifications">Notifications</string>
+    <string name="pref_notifications_summary">Notify when a new message arrives</string>
+    <string name="pref_vibrate">Vibrate</string>
+    <string name="pref_vibrate_summary">Also vibrate when a new message arrives</string>
+    <string name="pref_sound">Sound</string>
+    <string name="pref_sound_summary">Play ringtone with notification</string>
+    <string name="pref_conference_notifications">Conference notifications</string>
+    <string name="pref_conference_notifications_summary">Always notify when a new conference message arrives instead of only when highlighted</string>
+    <string name="pref_notification_grace_period">Notification grace period</string>
+    <string name="pref_notification_grace_period_summary">Disable notifications for a short time after a carbon copy was received</string>
+    <string name="pref_advanced_options">Advanced Options</string>
+    <string name="pref_never_send_crash">Never send crash reports</string>
+    <string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string>
+    <string name="pref_confirm_messages">Confirm Messages</string>
+    <string name="pref_confirm_messages_summary">Let your contact know when you have received and read a message</string>
+    <string name="pref_ui_options">UI Options</string>
+    <string name="openpgp_error">OpenKeychain reported an error</string>
+    <string name="error_decrypting_file">I/O Error decrypting file</string>
+    <string name="accept">Accept</string>
+    <string name="error">An error has occurred</string>
+    <string name="pref_grant_presence_updates">Grant presence updates</string>
+    <string name="pref_grant_presence_updates_summary">Preemptively grant and ask for presence subscription for contacts you created</string>
+    <string name="subscriptions">Subscriptions</string>
+    <string name="your_account">Your account</string>
+    <string name="keys">Keys</string>
+    <string name="send_presence_updates">Send presence updates</string>
+    <string name="receive_presence_updates">Receive presence updates</string>
+    <string name="ask_for_presence_updates">Ask for presence updates</string>
+    <string name="attach_choose_picture">Choose picture</string>
+    <string name="attach_take_picture">Take picture</string>
+    <string name="preemptively_grant">Preemptively grant subscription request</string>
+    <string name="error_not_an_image_file">The file you selected is not an image</string>
+    <string name="error_compressing_image">Error while converting the image file</string>
+    <string name="error_file_not_found">File not found</string>
+    <string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string>
+    <string name="error_security_exception_during_image_copy">The app you used to select this image did not provide us with enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small></string>
+    <string name="account_status_unknown">Unknown</string>
+    <string name="account_status_disabled">Temporarily disabled</string>
+    <string name="account_status_online">Online</string>
+    <string name="account_status_connecting">Connecting\u2026</string>
+    <string name="account_status_offline">Offline</string>
+    <string name="account_status_unauthorized">Unauthorized</string>
+    <string name="account_status_not_found">Server not found</string>
+    <string name="account_status_no_internet">No connectivity</string>
+    <string name="account_status_regis_fail">Registration failed</string>
+    <string name="account_status_regis_conflict">Username already in use</string>
+    <string name="account_status_regis_success">Registration completed</string>
+    <string name="account_status_regis_not_sup">Server does not support registration</string>
+    <string name="encryption_choice_none">Plain text</string>
+    <string name="encryption_choice_otr">OTR</string>
+    <string name="encryption_choice_pgp">OpenPGP</string>
+    <string name="mgmt_account_edit">Edit account</string>
+    <string name="mgmt_account_delete">Delete account</string>
+    <string name="mgmt_account_disable">Temporarily disable</string>
+    <string name="mgmt_account_publish_avatar">Publish avatar</string>
+    <string name="mgmt_account_publish_pgp">Publish OpenPGP public key</string>
+    <string name="mgmt_account_enable">Enable account</string>
+    <string name="mgmt_account_are_you_sure">Are you sure?</string>
+    <string name="mgmt_account_delete_confirm_text">If you delete your account your entire conversation history will be lost</string>
+    <string name="attach_record_voice">Record voice</string>
+    <string name="account_settings_jabber_id">Jabber ID</string>
+    <string name="account_settings_password">Password</string>
+    <string name="account_settings_example_jabber_id">username@example.com</string>
+    <string name="account_settings_confirm_password">Confirm password</string>
+    <string name="password">Password</string>
+    <string name="confirm_password">Confirm password</string>
+    <string name="passwords_do_not_match">Passwords do not match</string>
+    <string name="invalid_jid">This is not a valid Jabber ID</string>
+    <string name="error_out_of_memory">Out of memory. Image is too large</string>
+    <string name="add_phone_book_text">Do you want to add %s to your phones contact list?</string>
+    <string name="contact_status_online">online</string>
+    <string name="contact_status_free_to_chat">free to chat</string>
+    <string name="contact_status_away">away</string>
+    <string name="contact_status_extended_away">extended away</string>
+    <string name="contact_status_do_not_disturb">do not disturb</string>
+    <string name="contact_status_offline">offline</string>
+    <string name="muc_details_conference">Conference</string>
+    <string name="muc_details_other_members">Other Members</string>
+    <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string>
+    <string name="server_info_stream_management">XEP-0198: Stream Management</string>
+    <string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
+    <string name="server_info_available">available</string>
+    <string name="server_info_unavailable">unavailable</string>
+    <string name="missing_public_keys">Missing public key announcements</string>
+    <string name="last_seen_now">last seen just now</string>
+    <string name="last_seen_min">last seen 1 minute ago</string>
+    <string name="last_seen_mins">last seen %d minutes ago</string>
+    <string name="last_seen_hour">last seen 1 hour ago</string>
+    <string name="last_seen_hours">last seen %d hours ago</string>
+    <string name="last_seen_day">last seen 1 day ago</string>
+    <string name="last_seen_days">last seen %d days ago</string>
+    <string name="never_seen">never seen</string>
+    <string name="install_openkeychain">Encrypted message. Please install OpenKeychain to decrypt.</string>
+    <string name="unknown_otr_fingerprint">Unknown OTR fingerprint</string>
+    <string name="openpgp_messages_found">OpenPGP encrypted messages found</string>
+    <string name="reception_failed">Reception failed</string>
+    <string name="your_fingerprint">Your fingerprint</string>
+    <string name="otr_fingerprint">OTR fingerprint</string>
+    <string name="verify">Verify</string>
+    <string name="decrypt">Decrypt</string>
+    <string name="conferences">Conferences</string>
+    <string name="search">Search</string>
+    <string name="create_contact">Create Contact</string>
+    <string name="join_conference">Join Conference</string>
+    <string name="delete_contact">Delete Contact</string>
+    <string name="view_contact_details">View contact details</string>
+    <string name="create">Create</string>
+    <string name="contact_already_exists">The contact already exists</string>
+    <string name="join">Join</string>
+    <string name="conference_address">Conference address</string>
+    <string name="conference_address_example">room@conference.example.com</string>
+    <string name="save_as_bookmark">Save as bookmark</string>
+    <string name="delete_bookmark">Delete bookmark</string>
+    <string name="bookmark_already_exists">This bookmark already exists</string>
+    <string name="you">You</string>
+    <string name="action_edit_subject">Edit conference subject</string>
+    <string name="conference_not_found">Conference not found</string>
+    <string name="leave">Leave</string>
+    <string name="contact_added_you">Contact added you to contact list</string>
+    <string name="add_back">Add back</string>
+    <string name="contact_has_read_up_to_this_point">%s has read up to this point</string>
+    <string name="publish">Publish</string>
+    <string name="touch_to_choose_picture">Touch avatar to select picture from gallery</string>
+    <string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string>
+    <string name="publishing">Publishing&#8230;</string>
+    <string name="error_publish_avatar_server_reject">The server rejected your publication</string>
+    <string name="error_publish_avatar_converting">Something went wrong while converting your picture</string>
+    <string name="error_saving_avatar">Could not save avatar to disk</string>
+    <string name="or_long_press_for_default">(Or long press to bring back default)</string>
+    <string name="error_publish_avatar_no_server_support">Your server does not support the publication of avatars</string>
+    <string name="private_message">whispered</string>
+    <string name="private_message_to">to %s</string>
+    <string name="send_private_message_to">Send private message to %s</string>
+    <string name="connect">Connect</string>
+    <string name="account_already_exists">This account does already exist</string>
+    <string name="next">Next</string>
+    <string name="server_info_session_established">Current session established</string>
+    <string name="additional_information">Additional Information</string>
+    <string name="skip">Skip</string>
+    <string name="disable_notifications">Disable notifications</string>
+    <string name="disable_notifications_for_this_conversation">Disable notifications for this conversation</string>
+    <string name="notifications_disabled">Notifications are disabled</string>
+    <string name="enable">Enable</string>
+    <string name="conference_requires_password">Conference requires password</string>
+    <string name="enter_password">Enter password</string>
+    <string name="missing_presence_updates">Missing presence updates from contact</string>
+    <string name="request_presence_updates">Please request presence updates from your contact first.\n\n<small>This will be used to determine what client(s) your contact is using.</small></string>
+    <string name="request_now">Request now</string>
+    <string name="delete_fingerprint">Delete Fingerprint</string>
+    <string name="sure_delete_fingerprint">Are you sure you would like to delete this fingerprint?</string>
+    <string name="ignore">Ignore</string>
+    <string name="without_mutual_presence_updates"><b>Warning:</b> Sending this without mutual presence updates could cause unexpected problems.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string>
+    <string name="pref_encryption_settings">Encryption settings</string>
+    <string name="pref_force_encryption">Force end-to-end encryption</string>
+    <string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string>
+    <string name="pref_dont_save_encrypted">Don’t save encrypted messages</string>
+		<string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string>
+		<string name="pref_enable_legacy_ssl">Enable legacy SSL</string>
+		<string name="pref_enable_legacy_ssl_summary">Enables SSLv3 support for legacy servers. Warning: SSLv3 is considered insecure.</string>
+		<string name="pref_expert_options">Expert options</string>
+    <string name="pref_expert_options_summary">Please be careful with these</string>
+    <string name="pref_use_larger_font">Increase font size</string>
+    <string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</string>
+    <string name="pref_use_send_button_to_indicate_status">Send button indicates status</string>
+    <string name="pref_use_indicate_received">Request message receipts</string>
+    <string name="pref_use_indicate_received_summary">Received messages will be marked with a green tick if supported</string>
+    <string name="pref_use_send_button_to_indicate_status_summary">Colorize send button to indicate contact status</string>
+    <string name="pref_expert_options_other">Other</string>
+    <string name="pref_conference_name">Conference name</string>
+    <string name="pref_conference_name_summary">Use room’s subject instead of JID to identify conferences</string>
+    <string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
+    <string name="conference_banned">You are banned from this conference</string>
+    <string name="conference_members_only">This conference is members only</string>
+    <string name="conference_kicked">You have been kicked from this conference</string>
+    <string name="using_account">using account %s</string>
+    <string name="checking_image">Checking image on HTTP host</string>
+    <string name="image_file_deleted">The image file has been deleted</string>
+    <string name="not_connected_try_again">You are not connected. Try again later</string>
+    <string name="check_image_filesize">Check image file size</string>
+
+</resources>

conversations/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>

conversations/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>

conversations/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>

gradlew 🔗

@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

gradlew.bat 🔗

@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windowz variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+if "%@eval[2+2]" == "4" goto 4NT_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+goto execute

+

+:4NT_args

+@rem Get arguments from the 4NT Shell from JP Software

+set CMD_LINE_ARGS=%$

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

minidns 🔗

@@ -0,0 +1 @@
+Subproject commit 9e42bff01440c1351946a432126d5a1b87fb7c78

openpgpapilib 🔗

@@ -0,0 +1 @@
+Subproject commit 0be263d5d3effd2df5f976fa4a127017268749cc

settings.gradle 🔗

@@ -0,0 +1,4 @@
+include ':memorizingTrustManager'
+include ':minidns'
+include ':openpgpapilib'
+include ':conversations'