Merge branch 'development'

iNPUTmice created

Change summary

AndroidManifest.xml                                            |  13 
CHANGELOG.md                                                   |   5 
README.md                                                      |   6 
art/ic_action_copy.svg                                         | 108 +
art/render.rb                                                  |   2 
res/drawable-hdpi/ic_action_copy.png                           |   0 
res/drawable-mdpi/ic_action_copy.png                           |   0 
res/drawable-xhdpi/ic_action_copy.png                          |   0 
res/drawable-xxhdpi/ic_action_copy.png                         |   0 
res/layout-w360dp/fragment_conversations_overview.xml          |   4 
res/layout-w384dp/fragment_conversations_overview.xml          |   2 
res/layout-w600dp/fragment_conversations_overview.xml          |   4 
res/layout-w960dp/fragment_conversations_overview.xml          |  18 
res/layout/activity_edit_account.xml                           |  39 
res/layout/dialog_verify_otr.xml                               |   2 
res/layout/fragment_conversation.xml                           |   2 
res/layout/fragment_conversations_overview.xml                 |   2 
res/layout/message_null.xml                                    |   3 
res/layout/message_received.xml                                |  10 
res/layout/message_sent.xml                                    |  20 
res/values-de/arrays.xml                                       |   7 
res/values-de/strings.xml                                      |   6 
res/values-es/strings.xml                                      |  11 
res/values-eu/arrays.xml                                       |   7 
res/values-eu/strings.xml                                      |   4 
res/values-iw/arrays.xml                                       |   0 
res/values-iw/strings.xml                                      |   0 
res/values-zh-rCN/arrays.xml                                   |  39 
res/values-zh-rCN/strings.xml                                  |   0 
res/values-zh-rTW/arrays.xml                                   |  39 
res/values-zh-rTW/strings.xml                                  | 263 +++
res/values/strings.xml                                         |   6 
res/xml/preferences.xml                                        |  24 
src/eu/siacs/conversations/Config.java                         |   2 
src/eu/siacs/conversations/entities/Bookmark.java              | 115 
src/eu/siacs/conversations/entities/Contact.java               |   1 
src/eu/siacs/conversations/entities/Conversation.java          | 116 +
src/eu/siacs/conversations/entities/Message.java               |   6 
src/eu/siacs/conversations/entities/MucOptions.java            |  63 
src/eu/siacs/conversations/entities/Roster.java                |  13 
src/eu/siacs/conversations/generator/AbstractGenerator.java    |   4 
src/eu/siacs/conversations/generator/MessageGenerator.java     |   8 
src/eu/siacs/conversations/parser/AbstractParser.java          |   2 
src/eu/siacs/conversations/parser/IqParser.java                |   3 
src/eu/siacs/conversations/parser/MessageParser.java           |  81 
src/eu/siacs/conversations/parser/PresenceParser.java          |   6 
src/eu/siacs/conversations/persistance/DatabaseBackend.java    |  24 
src/eu/siacs/conversations/persistance/FileBackend.java        |  17 
src/eu/siacs/conversations/services/EventReceiver.java         |   3 
src/eu/siacs/conversations/services/ImageProvider.java         |   2 
src/eu/siacs/conversations/services/NotificationService.java   | 241 +++
src/eu/siacs/conversations/services/XmppConnectionService.java | 154 +
src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java   |   2 
src/eu/siacs/conversations/ui/ContactDetailsActivity.java      |  14 
src/eu/siacs/conversations/ui/ConversationActivity.java        | 280 ++-
src/eu/siacs/conversations/ui/ConversationFragment.java        |  55 
src/eu/siacs/conversations/ui/EditAccountActivity.java         |  83 +
src/eu/siacs/conversations/ui/ManageAccountActivity.java       |   8 
src/eu/siacs/conversations/ui/SettingsActivity.java            |  18 
src/eu/siacs/conversations/ui/StartConversationActivity.java   |  91 +
src/eu/siacs/conversations/ui/XmppActivity.java                |  18 
src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java |   8 
src/eu/siacs/conversations/ui/adapter/ListItemAdapter.java     |   4 
src/eu/siacs/conversations/ui/adapter/MessageAdapter.java      |  12 
src/eu/siacs/conversations/utils/DNSHelper.java                |   8 
src/eu/siacs/conversations/utils/UIHelper.java                 | 178 --
src/eu/siacs/conversations/xmpp/XmppConnection.java            |  46 
src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java   |  12 
68 files changed, 1,690 insertions(+), 654 deletions(-)

Detailed changes

AndroidManifest.xml 🔗

@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="eu.siacs.conversations"
-    android:versionCode="28"
-    android:versionName="0.7.2" >
+    android:versionCode="31"
+    android:versionName="0.7.3" >
 
     <uses-sdk
         android:minSdkVersion="14"
@@ -40,7 +40,6 @@
 
         <activity
             android:name="eu.siacs.conversations.ui.ConversationActivity"
-            android:configChanges="orientation|screenSize"
             android:label="@string/title_activity_conversations"
             android:launchMode="singleTask"
             android:windowSoftInputMode="stateHidden" >
@@ -63,6 +62,14 @@
                 <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"

CHANGELOG.md 🔗

@@ -1,5 +1,10 @@
 ###Changelog
 
+####Version 0.7.3
+* revised tablet ui
+* internal rewrites
+* bug fixes
+
 ####Version 0.7.2
 * show full timestamp in messages
 * brought back option to use JID to identify conferences

README.md 🔗

@@ -69,6 +69,7 @@ These XEPs are - as of now:
 * [Ilia Rostovtsev](https://github.com/qooob) (Russian)
 * [Jelmer Vernooij](https://github.com/jelmer) (Dutch)
 * [Anders Sandblad](https://github.com/andersruneson) (Swedish)
+* [Aizaz AZ](http://www.linkedin.com/in/aizazhaider) (Chinese)
 
 ##FAQ
 ###General
@@ -81,7 +82,7 @@ The more convenient way - which not only gives you automatic updates but also
 supports the further development of Conversations - is to buy the App in the Google
 [Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations).
 ####I don't have a Google Account but I would still like to make a contribution
-I accept donations over PayPal and BitCoin. For donations via PayPal you can use the email address donate@siacs.eu or the button below.
+I accept donations over PayPal, BitCoin and Flattr. For donations via PayPal you can use the email address donate@siacs.eu or the button below.
 
 [![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CW3SYT3KG5PDL)
 
@@ -91,6 +92,9 @@ transfer (SEPA).
 
 My Bitcoin Address is: 1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu
 
+
+[![Flattr this!](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=inputmice&url=https%3A%2F%2Fgithub.com%2Fsiacs%2FConversations)
+
 ####How do I create an account?
 XMPP like email for example is a federated protocol which means that there is
 not one company you can create your 'official xmpp account' with but there are

art/ic_action_copy.svg 🔗

@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="128"
+   height="128"
+   id="svg4066"
+   version="1.1"
+   inkscape:version="0.48.5 r10040"
+   sodipodi:docname="ic_action_copy.svg">
+  <defs
+     id="defs4068" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.54117647"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.67"
+     inkscape:cx="51.750573"
+     inkscape:cy="57.547291"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     borderlayer="false"
+     inkscape:window-width="1035"
+     inkscape:window-height="853"
+     inkscape:window-x="369"
+     inkscape:window-y="3"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata4071">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-924.36218)">
+    <rect
+       ry="0"
+       height="91.708199"
+       width="71.625328"
+       stroke-miterlimit="4"
+       y="952.36743"
+       x="42.730034"
+       id="rect10"
+       style="fill:none;stroke:#000000;stroke-width:8.6679945;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.54117647;stroke-dasharray:none"
+       inkscape:transform-center-x="-21.391573"
+       inkscape:transform-center-y="28.294015" />
+    <path
+       style="fill:#000000;fill-opacity:0.5411765;fill-rule:evenodd;stroke:#000000;stroke-width:0.41999999999999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117649999999995;stroke-miterlimit:4;stroke-dasharray:none"
+       d="m 20.552281,933.36985 0,0.0209 -0.128276,0 -0.399078,99.83215 0.213792,0 0,0.1463 13.212333,-0.021 0.05701,-8.1392 -4.076297,0.011 0.327814,-84.87039 58.436429,0 0.0285,3.427 11.10293,0 -0.0855,-9.25707 -0.057,0 0,-1.1493 -78.63263,0 z"
+       id="rect12-6"
+       inkscape:connector-curvature="0" />
+    <rect
+       height="4.7259107"
+       width="37.242958"
+       y="967.49921"
+       x="50.137043"
+       id="rect12"
+       style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.23799089px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647" />
+    <rect
+       style="fill:#000000;fill-opacity:0.54117647000000002;fill-rule:evenodd;stroke:#000000;stroke-width:0.274;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647000000002;stroke-miterlimit:4;stroke-dasharray:none"
+       id="rect4272"
+       x="50.137043"
+       y="982.49921"
+       width="49.452484"
+       height="4.7259107" />
+    <rect
+       height="4.7259107"
+       width="43.542446"
+       y="997.49921"
+       x="50.137043"
+       id="rect4274"
+       style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.2573325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647" />
+    <rect
+       style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.25050664px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647"
+       id="rect4276"
+       x="50.137043"
+       y="1012.4992"
+       width="41.263123"
+       height="4.7259107" />
+    <rect
+       height="4.7259107"
+       width="49.397911"
+       y="1027.4993"
+       x="50.137043"
+       id="rect4278"
+       style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.27408957px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647" />
+  </g>
+</svg>

art/render.rb 🔗

@@ -1,6 +1,6 @@
 #!/bin/env ruby
 resolutions={'mdpi'=> 1, 'hdpi' => 1.5, 'xhdpi' => 2, 'xxhdpi' => 3}
-images = { 'conversations.svg' => ['ic_launcher',48], 'conversations_baloon.svg' => ['ic_activity', 32], 'conversations_mono.svg' => ['ic_notification',24], 'ic_received_indicator.svg' => ['ic_received_indicator',12] }
+images = { 'conversations.svg' => ['ic_launcher', 48], 'conversations_baloon.svg' => ['ic_activity', 32], 'conversations_mono.svg' => ['ic_notification', 24], 'ic_received_indicator.svg' => ['ic_received_indicator', 12], 'ic_action_copy.svg' => ['ic_action_copy', 32] }
 images.each do |source, result|
 	resolutions.each do |name, factor|
 		size = factor * result[1]

res/layout-sw360dp/fragment_conversations_overview.xml → res/layout-w360dp/fragment_conversations_overview.xml 🔗

@@ -1,11 +1,11 @@
 <android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/slidingpanelayout"
+    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="324dp"
+        android:layout_width="300dp"
         android:layout_height="match_parent"
         android:background="@color/primarybackground"
         android:orientation="vertical" >

res/layout-sw384dp/fragment_conversations_overview.xml → res/layout-w384dp/fragment_conversations_overview.xml 🔗

@@ -1,5 +1,5 @@
 <android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/slidingpanelayout"
+    android:id="@+id/content_view_spl"
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 

res/layout-sw600dp/fragment_conversations_overview.xml → res/layout-w600dp/fragment_conversations_overview.xml 🔗

@@ -1,11 +1,11 @@
 <android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/slidingpanelayout"
+    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="240dp"
+        android:layout_width="400dp"
         android:layout_height="match_parent"
         android:background="@color/primarybackground"
         android:orientation="vertical" >

res/layout-sw720dp/fragment_conversations_overview.xml → res/layout-w960dp/fragment_conversations_overview.xml 🔗

@@ -1,12 +1,14 @@
-<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/slidingpanelayout"
+<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:layout_height="match_parent"
+    android:orientation="horizontal"
+    android:baselineAligned="false">
 
     <LinearLayout
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="288dp"
+        android:layout_width="0dp"
         android:layout_height="match_parent"
+        android:layout_weight="1"
         android:background="@color/primarybackground"
         android:orientation="vertical" >
 
@@ -21,10 +23,10 @@
 
     <LinearLayout
         android:id="@+id/selected_conversation"
-        android:layout_width="fill_parent"
+        android:layout_width="0dp"
+        android:layout_weight="2"
         android:layout_height="match_parent"
-        android:layout_weight="1"
         android:orientation="vertical" >
     </LinearLayout>
 
-</android.support.v4.widget.SlidingPaneLayout>
+</LinearLayout>

res/layout/activity_edit_account.xml 🔗

@@ -180,13 +180,38 @@
                     android:textSize="?attr/TextSizeHeadline"
                     android:textStyle="bold" />
 
-                <TextView
-                    android:id="@+id/otr_fingerprint"
+                <RelativeLayout
                     android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="8dp"
-                    android:textSize="?attr/TextSizeBody"
-                    android:typeface="monospace" />
+                    android:layout_height="match_parent"
+                    android:layout_marginTop="8dp">
+                    <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"
+                        android:layout_centerVertical="true">
+
+                        <TextView
+                            android:id="@+id/otr_fingerprint"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:textSize="?attr/TextSizeBody"
+                            android:typeface="monospace" />
+                    </LinearLayout>
+
+                    <ImageButton
+                        android:id="@+id/action_copy_to_clipboard"
+                        android:layout_width="32dp"
+                        android:layout_height="32dp"
+                        android:background="?android:selectableItemBackground"
+                        android:layout_alignParentRight="true"
+                        android:layout_centerVertical="true"
+                        android:padding="4dp"
+                        android:scaleType="fitXY"
+                        android:src="@drawable/ic_action_copy"
+                        android:visibility="invisible" />
+                </RelativeLayout>
             </LinearLayout>
         </LinearLayout>
     </ScrollView>
@@ -226,4 +251,4 @@
             android:textColor="@color/secondarytext" />
     </LinearLayout>
 
-</RelativeLayout>
+</RelativeLayout>

res/layout/dialog_verify_otr.xml 🔗

@@ -11,7 +11,7 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:paddingTop="8dp"
-        android:text="Jabber ID"
+        android:text="@string/account_settings_jabber_id"
         android:textColor="@color/primarytext"
         android:textSize="?attr/TextSizeHeadline" />
 

res/layout/fragment_conversation.xml 🔗

@@ -37,7 +37,7 @@
             android:layout_toLeftOf="@+id/textSendButton"
             android:background="@color/primarybackground"
             android:ems="10"
-            android:imeOptions="flagNoExtractUi"
+            android:imeOptions="flagNoExtractUi|actionSend"
             android:inputType="textShortMessage|textMultiLine|textCapSentences"
             android:minHeight="48dp"
             android:minLines="1"

res/layout/fragment_conversations_overview.xml 🔗

@@ -1,5 +1,5 @@
 <android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/slidingpanelayout"
+    android:id="@+id/content_view_spl"
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 

res/layout/message_null.xml 🔗

@@ -1,6 +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="wrap_content" >
+    android:layout_height="0dp"
+    android:background="#00000000">
 
 </RelativeLayout>

res/layout/message_received.xml 🔗

@@ -54,7 +54,7 @@
                 android:text="@string/download_image"
                 android:visibility="gone" />
 
-           <LinearLayout
+            <LinearLayout
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:orientation="horizontal"
@@ -65,12 +65,10 @@
                     android:layout_width="?attr/TextSizeInfo"
                     android:layout_height="?attr/TextSizeInfo"
                     android:layout_gravity="center_vertical"
-                    android:gravity="center_vertical"
-                    android:src="@drawable/ic_secure_indicator"
                     android:layout_marginRight="4sp"
-                    android:alpha="0.54"/>
-                
-              
+                    android:alpha="0.54"
+                    android:gravity="center_vertical"
+                    android:src="@drawable/ic_secure_indicator" />
 
                 <TextView
                     android:id="@+id/message_time"

res/layout/message_sent.xml 🔗

@@ -63,27 +63,25 @@
                     android:textColor="@color/secondarytext"
                     android:textSize="?attr/TextSizeInfo" />
 
-                 <ImageView
+                <ImageView
                     android:id="@+id/security_indicator"
                     android:layout_width="?attr/TextSizeInfo"
                     android:layout_height="?attr/TextSizeInfo"
                     android:layout_gravity="center_vertical"
-                    android:gravity="center_vertical"
-                    android:src="@drawable/ic_secure_indicator"
                     android:layout_marginLeft="4sp"
-                    android:alpha="0.54"/>
-                
+                    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:gravity="center_vertical"
-                    android:src="@drawable/ic_received_indicator"
                     android:layout_marginLeft="4sp"
-                    android:alpha="0.54"/>
-
-               
+                    android:alpha="0.54"
+                    android:gravity="center_vertical"
+                    android:src="@drawable/ic_received_indicator" />
             </LinearLayout>
         </LinearLayout>
     </LinearLayout>
@@ -99,4 +97,4 @@
         android:scaleType="fitXY"
         android:src="@drawable/ic_profile" />
 
-</RelativeLayout>
+</RelativeLayout>

res/values-de/arrays.xml 🔗

@@ -20,5 +20,12 @@
         <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>

res/values-de/strings.xml 🔗

@@ -259,5 +259,11 @@
     <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>

res/values-es/strings.xml 🔗

@@ -254,5 +254,16 @@
     <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>

res/values-eu/arrays.xml 🔗

@@ -26,8 +26,9 @@
         <item>2 ordu</item>
         <item>8 ordu</item>
         <item>abisatu arte</item>
-        </string-array>
-        <integer-array name="mute_options_durations">
+    </string-array>
+
+    <integer-array name="mute_options_durations">
         <item>1800</item>
         <item>3600</item>
         <item>7200</item>
@@ -35,4 +36,4 @@
         <item>-1</item>
     </integer-array>
 
-</resources>
+</resources>

res/values-eu/strings.xml 🔗

@@ -256,5 +256,5 @@
     <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_send_button_to_indicate_status_summary">Bidaltze botoia koloreztatu kontaktu baten egoera adierazteko</string>
-    
-</resources>
+
+</resources>

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>

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>

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>

res/values/strings.xml 🔗

@@ -261,5 +261,9 @@
     <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>
 
-</resources>
+</resources>

res/xml/preferences.xml 🔗

@@ -52,15 +52,9 @@
 
         <CheckBoxPreference
             android:dependency="show_notification"
-            android:key="notify_in_conversation_when_highlighted"
+            android:key="always_notify_in_conference"
             android:summary="@string/pref_conference_notifications_summary"
             android:title="@string/pref_conference_notifications" />
-        <CheckBoxPreference
-            android:defaultValue="true"
-            android:dependency="show_notification"
-            android:key="notification_grace_period_after_carbon_received"
-            android:summary="@string/pref_notification_grace_period_summary"
-            android:title="@string/pref_notification_grace_period" />
     </PreferenceCategory>
     <PreferenceCategory android:title="@string/pref_ui_options" >
         <CheckBoxPreference
@@ -95,13 +89,13 @@
                     android:summary="@string/pref_dont_save_encrypted_summary"
                     android:title="@string/pref_dont_save_encrypted" />
             </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>
+            <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
@@ -111,4 +105,4 @@
             android:title="@string/pref_never_send_crash" />
     </PreferenceCategory>
 
-</PreferenceScreen>
+</PreferenceScreen>

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

@@ -10,7 +10,7 @@ public final class Config {
 	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 CARBON_GRACE_PERIOD = 120;
 
 	public static final int AVATAR_SIZE = 192;
 	public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;

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

@@ -7,46 +7,35 @@ import android.graphics.Bitmap;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xml.Element;
 
-public class Bookmark implements ListItem {
+public class Bookmark extends Element implements ListItem {
 
 	private Account account;
-	private String jid;
-	private String nick;
-	private String name;
-	private String password;
-	private boolean autojoin;
-	private boolean providePassword;
 	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;
-		this.jid = jid;
 	}
 
 	public static Bookmark parse(Element element, Account account) {
-		Bookmark bookmark = new Bookmark(account, element.getAttribute("jid"));
-		bookmark.setName(element.getAttribute("name"));
-		String autojoin = element.getAttribute("autojoin");
-		if (autojoin != null
-				&& (autojoin.equals("true") || autojoin.equals("1"))) {
-			bookmark.setAutojoin(true);
-		} else {
-			bookmark.setAutojoin(false);
-		}
-		Element nick = element.findChild("nick");
-		if (nick != null) {
-			bookmark.setNick(nick.getContent());
-		}
-		Element password = element.findChild("password");
-		if (password != null) {
-			bookmark.setPassword(password.getContent());
-			bookmark.setProvidePassword(true);
-		}
+		Bookmark bookmark = new Bookmark(account);
+		bookmark.setAttributes(element.getAttributes());
+		bookmark.setChildren(element.getChildren());
 		return bookmark;
 	}
 
 	public void setAutojoin(boolean autojoin) {
-		this.autojoin = autojoin;
+		if (autojoin) {
+			this.setAttribute("autojoin", "true");
+		} else {
+			this.setAttribute("autojoin", "false");
+		}
 	}
 
 	public void setName(String name) {
@@ -54,15 +43,18 @@ public class Bookmark implements ListItem {
 	}
 
 	public void setNick(String nick) {
-		this.nick = nick;
+		Element element = this.findChild("nick");
+		if (element == null) {
+			element = this.addChild("nick");
+		}
+		element.setContent(nick);
 	}
 
 	public void setPassword(String password) {
-		this.password = password;
-	}
-
-	private void setProvidePassword(boolean providePassword) {
-		this.providePassword = providePassword;
+		Element element = this.findChild("password");
+		if (element != null) {
+			element.setContent(password);
+		}
 	}
 
 	@Override
@@ -76,32 +68,45 @@ public class Bookmark implements ListItem {
 		if (this.mJoinedConversation != null
 				&& (this.mJoinedConversation.getMucOptions().getSubject() != null)) {
 			return this.mJoinedConversation.getMucOptions().getSubject();
-		} else if (name != null) {
-			return name;
+		} else if (getName() != null) {
+			return getName();
 		} else {
-			return this.jid.split("@")[0];
+			return this.getJid().split("@")[0];
 		}
 	}
 
 	@Override
 	public String getJid() {
-		return this.jid.toLowerCase(Locale.US);
+		String jid = this.getAttribute("jid");
+		if (jid != null) {
+			return jid.toLowerCase(Locale.US);
+		} else {
+			return null;
+		}
 	}
 
 	public String getNick() {
-		return this.nick;
+		Element nick = this.findChild("nick");
+		if (nick != null) {
+			return nick.getContent();
+		} else {
+			return null;
+		}
 	}
 
 	public boolean autojoin() {
-		return autojoin;
+		String autojoin = this.getAttribute("autojoin");
+		return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin
+				.equalsIgnoreCase("1")));
 	}
 
 	public String getPassword() {
-		return this.password;
-	}
-
-	public boolean isProvidePassword() {
-		return this.providePassword;
+		Element password = this.findChild("password");
+		if (password != null) {
+			return password.getContent();
+		} else {
+			return null;
+		}
 	}
 
 	public boolean match(String needle) {
@@ -131,27 +136,7 @@ public class Bookmark implements ListItem {
 	}
 
 	public String getName() {
-		return name;
-	}
-
-	public Element toElement() {
-		Element element = new Element("conference");
-		element.setAttribute("jid", this.getJid());
-		if (this.getName() != null) {
-			element.setAttribute("name", this.getName());
-		}
-		if (this.autojoin) {
-			element.setAttribute("autojoin", "true");
-		} else {
-			element.setAttribute("autojoin", "false");
-		}
-		if (this.nick != null) {
-			element.addChild("nick").setContent(this.nick);
-		}
-		if (this.password != null && isProvidePassword()) {
-			element.addChild("password").setContent(this.password);
-		}
-		return element;
+		return this.getAttribute("name");
 	}
 
 	public void unregisterConversation() {

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

@@ -156,6 +156,7 @@ public class Contact implements ListItem {
 
 	public void clearPresences() {
 		this.presences.clearPresences();
+		this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
 	}
 
 	public int getMostAvailableStatus() {

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

@@ -4,6 +4,9 @@ import java.security.interfaces.DSAPublicKey;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.UIHelper;
 
@@ -36,6 +39,11 @@ public class Conversation extends AbstractEntity {
 	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;
@@ -45,7 +53,7 @@ public class Conversation extends AbstractEntity {
 	private long created;
 	private int mode;
 
-	private long mutedTill = 0;
+	private JSONObject attributes = new JSONObject();
 
 	private String nextPresence;
 
@@ -56,12 +64,11 @@ public class Conversation extends AbstractEntity {
 
 	private transient String otrFingerprint = null;
 
-	private int nextMessageEncryption = -1;
 	private String nextMessage;
 
 	private transient MucOptions mucOptions = null;
 
-	private transient String latestMarkableMessageId;
+	//private transient String latestMarkableMessageId;
 
 	private byte[] symmetricKey;
 
@@ -73,13 +80,13 @@ public class Conversation extends AbstractEntity {
 			int mode) {
 		this(java.util.UUID.randomUUID().toString(), name, null, account
 				.getUuid(), contactJid, System.currentTimeMillis(),
-				STATUS_AVAILABLE, mode);
+				STATUS_AVAILABLE, mode, "");
 		this.account = account;
 	}
 
 	public Conversation(String uuid, String name, String contactUuid,
 			String accountUuid, String contactJid, long created, int status,
-			int mode) {
+			int mode, String attributes) {
 		this.uuid = uuid;
 		this.name = name;
 		this.contactUuid = contactUuid;
@@ -88,6 +95,14 @@ public class Conversation extends AbstractEntity {
 		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() {
@@ -123,10 +138,20 @@ public class Conversation extends AbstractEntity {
 		}
 	}
 
-	public String popLatestMarkableMessageId() {
-		String id = this.latestMarkableMessageId;
-		this.latestMarkableMessageId = null;
-		return id;
+	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() {
@@ -198,6 +223,7 @@ public class Conversation extends AbstractEntity {
 		values.put(CREATED, created);
 		values.put(STATUS, status);
 		values.put(MODE, mode);
+		values.put(ATTRIBUTES, attributes.toString());
 		return values;
 	}
 
@@ -209,7 +235,8 @@ public class Conversation extends AbstractEntity {
 				cursor.getString(cursor.getColumnIndex(CONTACTJID)),
 				cursor.getLong(cursor.getColumnIndex(CREATED)),
 				cursor.getInt(cursor.getColumnIndex(STATUS)),
-				cursor.getInt(cursor.getColumnIndex(MODE)));
+				cursor.getInt(cursor.getColumnIndex(MODE)),
+				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 	}
 
 	public void setStatus(int status) {
@@ -229,8 +256,8 @@ public class Conversation extends AbstractEntity {
 		if (this.otrSession != null) {
 			return this.otrSession;
 		} else {
-			SessionID sessionId = new SessionID(
-					this.getContactJid().split("/",2)[0], presence, "xmpp");
+			SessionID sessionId = new SessionID(this.getContactJid().split("/",
+					2)[0], presence, "xmpp");
 			this.otrSession = new SessionImpl(sessionId, getAccount()
 					.getOtrEngine(service));
 			try {
@@ -345,7 +372,8 @@ public class Conversation extends AbstractEntity {
 	}
 
 	public int getNextEncryption(boolean force) {
-		if (this.nextMessageEncryption == -1) {
+		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) {
@@ -363,16 +391,16 @@ public class Conversation extends AbstractEntity {
 				return latest;
 			}
 		}
-		if (this.nextMessageEncryption == Message.ENCRYPTION_NONE && force
+		if (next == Message.ENCRYPTION_NONE && force
 				&& getMode() == MODE_SINGLE) {
 			return Message.ENCRYPTION_OTR;
 		} else {
-			return this.nextMessageEncryption;
+			return next;
 		}
 	}
 
 	public void setNextEncryption(int encryption) {
-		this.nextMessageEncryption = encryption;
+		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
 	}
 
 	public String getNextMessage() {
@@ -387,12 +415,6 @@ public class Conversation extends AbstractEntity {
 		this.nextMessage = message;
 	}
 
-	public void setLatestMarkableMessageId(String id) {
-		if (id != null) {
-			this.latestMarkableMessageId = id;
-		}
-	}
-
 	public void setSymmetricKey(byte[] key) {
 		this.symmetricKey = key;
 	}
@@ -433,11 +455,55 @@ public class Conversation extends AbstractEntity {
 		return false;
 	}
 
-	public void setMutedTill(long mutedTill) {
-		this.mutedTill = mutedTill;
+	public void setMutedTill(long value) {
+		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 	}
 
 	public boolean isMuted() {
-		return SystemClock.elapsedRealtime() < this.mutedTill;
+		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;
+			}
+		}
 	}
 }

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

@@ -57,9 +57,9 @@ public class Message extends AbstractEntity {
 	protected boolean read = true;
 	protected String remoteMsgId = null;
 
-	protected transient Conversation conversation = null;
-
-	protected transient Downloadable downloadable = null;
+	protected Conversation conversation = null;
+	protected Downloadable downloadable = null;
+	public boolean markable = false;
 
 	private Message() {
 

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

@@ -15,6 +15,13 @@ public class MucOptions {
 	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);
@@ -108,7 +115,6 @@ public class MucOptions {
 	private String subject = null;
 	private String joinnick;
 	private String password = null;
-	private boolean passwordChanged = false;
 
 	public MucOptions(Account account) {
 		this.account = account;
@@ -134,7 +140,7 @@ public class MucOptions {
 	}
 
 	public void processPacket(PresencePacket packet, PgpEngine pgp) {
-		String[] fromParts = packet.getFrom().split("/",2);
+		String[] fromParts = packet.getFrom().split("/", 2);
 		if (fromParts.length >= 2) {
 			String name = fromParts[1];
 			String type = packet.getAttribute("type");
@@ -158,10 +164,6 @@ public class MucOptions {
 						}
 						aboutToRename = false;
 					}
-					if (conversation.getBookmark() != null
-							&& conversation.getBookmark().isProvidePassword()) {
-						this.passwordChanged = false;
-					}
 				} else {
 					addUser(user);
 				}
@@ -179,11 +181,27 @@ public class MucOptions {
 								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]);
+				deleteUser(packet.getAttribute("from").split("/", 2)[1]);
 			} else if (type.equals("error")) {
 				Element error = packet.findChild("error");
-				if (error.hasChild("conflict")) {
+				if (error != null && error.hasChild("conflict")) {
 					if (aboutToRename) {
 						if (renameListener != null) {
 							renameListener.onRename(false);
@@ -193,12 +211,13 @@ public class MucOptions {
 					} else {
 						this.error = ERROR_NICK_IN_USE;
 					}
-				} else if (error.hasChild("not-authorized")) {
-					if (conversation.getBookmark() != null
-							&& conversation.getBookmark().isProvidePassword()) {
-						this.passwordChanged = true;
-					}
+				} 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;
 				}
 			}
 		}
@@ -209,7 +228,7 @@ public class MucOptions {
 	}
 
 	public String getProposedNick() {
-		String[] mucParts = conversation.getContactJid().split("/",2);
+		String[] mucParts = conversation.getContactJid().split("/", 2);
 		if (conversation.getBookmark() != null
 				&& conversation.getBookmark().getNick() != null) {
 			return conversation.getBookmark().getNick();
@@ -309,7 +328,7 @@ public class MucOptions {
 	}
 
 	public String getJoinJid() {
-		return this.conversation.getContactJid().split("/",2)[0] + "/"
+		return this.conversation.getContactJid().split("/", 2)[0] + "/"
 				+ this.joinnick;
 	}
 
@@ -323,7 +342,9 @@ public class MucOptions {
 	}
 
 	public String getPassword() {
-		if (conversation.getBookmark() != null
+		this.password = conversation
+				.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
+		if (this.password == null && conversation.getBookmark() != null
 				&& conversation.getBookmark().getPassword() != null) {
 			return conversation.getBookmark().getPassword();
 		} else {
@@ -332,16 +353,12 @@ public class MucOptions {
 	}
 
 	public void setPassword(String password) {
-		if (conversation.getBookmark() != null
-				&& conversation.getBookmark().isProvidePassword()) {
+		if (conversation.getBookmark() != null) {
 			conversation.getBookmark().setPassword(password);
 		} else {
 			this.password = password;
 		}
+		conversation
+				.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
 	}
-
-	public boolean isPasswordChanged() {
-		return this.passwordChanged;
-	}
-
 }

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

@@ -14,13 +14,18 @@ public class Roster {
 		this.account = account;
 	}
 
-	public boolean hasContact(String jid) {
-		String cleanJid = jid.split("/",2)[0];
-		return contacts.containsKey(cleanJid);
+	public Contact getContactAsShownInRoster(String jid) {
+		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());
+		String cleanJid = jid.split("/", 2)[0].toLowerCase(Locale.getDefault());
 		if (contacts.containsKey(cleanJid)) {
 			return contacts.get(cleanJid);
 		} else {

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

@@ -21,9 +21,9 @@ public abstract class AbstractGenerator {
 			"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;
 	}

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

@@ -34,7 +34,7 @@ public class MessageGenerator extends AbstractGenerator {
 			packet.setTo(message.getCounterpart());
 			packet.setType(MessagePacket.TYPE_CHAT);
 		} else {
-			packet.setTo(message.getCounterpart().split("/",2)[0]);
+			packet.setTo(message.getCounterpart().split("/", 2)[0]);
 			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 		}
 		packet.setFrom(account.getFullJid());
@@ -134,7 +134,7 @@ public class MessageGenerator extends AbstractGenerator {
 			String subject) {
 		MessagePacket packet = new MessagePacket();
 		packet.setType(MessagePacket.TYPE_GROUPCHAT);
-		packet.setTo(conversation.getContactJid().split("/",2)[0]);
+		packet.setTo(conversation.getContactJid().split("/", 2)[0]);
 		Element subjectChild = new Element("subject");
 		subjectChild.setContent(subject);
 		packet.addChild(subjectChild);
@@ -148,13 +148,13 @@ public class MessageGenerator extends AbstractGenerator {
 		packet.setTo(contact);
 		packet.setFrom(conversation.getAccount().getFullJid());
 		Element x = packet.addChild("x", "jabber:x:conference");
-		x.setAttribute("jid", conversation.getContactJid().split("/",2)[0]);
+		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.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");

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

@@ -60,7 +60,7 @@ public abstract class AbstractParser {
 
 	protected void updateLastseen(Element packet, Account account,
 			boolean presenceOverwrite) {
-		String[] fromParts = packet.getAttribute("from").split("/",2);
+		String[] fromParts = packet.getAttribute("from").split("/", 2);
 		String from = fromParts[0];
 		String presence = null;
 		if (fromParts.length >= 2) {

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

@@ -73,6 +73,9 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
 			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)) {

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

@@ -1,13 +1,12 @@
 package eu.siacs.conversations.parser;
 
-import android.os.SystemClock;
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
-import eu.siacs.conversations.Config;
 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;
@@ -17,9 +16,6 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
 public class MessageParser extends AbstractParser implements
 		OnMessagePacketReceived {
-
-	private long lastCarbonMessageReceived = -(Config.CARBON_GRACE_PERIOD * 1000);
-
 	public MessageParser(XmppConnectionService service) {
 		super(service);
 	}
@@ -28,7 +24,6 @@ public class MessageParser extends AbstractParser implements
 		String[] fromParts = packet.getFrom().split("/", 2);
 		Conversation conversation = mXmppConnectionService
 				.findOrCreateConversation(account, fromParts[0], false);
-		conversation.setLatestMarkableMessageId(getMarkableMessageId(packet));
 		updateLastseen(packet, account, true);
 		String pgpBody = getPgpBody(packet);
 		Message finishedMessage;
@@ -41,6 +36,7 @@ public class MessageParser extends AbstractParser implements
 					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);
@@ -71,7 +67,7 @@ public class MessageParser extends AbstractParser implements
 		updateLastseen(packet, account, true);
 		String body = packet.getBody();
 		if (body.matches("^\\?OTRv\\d*\\?")) {
-			conversation.resetOtrSession();
+			conversation.endOtrIfNeeded();
 		}
 		if (!conversation.hasValidOtrSession()) {
 			if (properlyAddressed) {
@@ -112,13 +108,12 @@ public class MessageParser extends AbstractParser implements
 				conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
 				return null;
 			}
-			conversation
-					.setLatestMarkableMessageId(getMarkableMessageId(packet));
 			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();
@@ -160,7 +155,6 @@ public class MessageParser extends AbstractParser implements
 			status = Message.STATUS_RECEIVED;
 		}
 		String pgpBody = getPgpBody(packet);
-		conversation.setLatestMarkableMessageId(getMarkableMessageId(packet));
 		Message finishedMessage;
 		if (pgpBody == null) {
 			finishedMessage = new Message(conversation, counterPart,
@@ -170,6 +164,7 @@ public class MessageParser extends AbstractParser implements
 					Message.ENCRYPTION_PGP, status);
 		}
 		finishedMessage.setRemoteMsgId(packet.getId());
+		finishedMessage.markable = isMarkable(packet);
 		if (status == Message.STATUS_RECEIVED) {
 			finishedMessage.setTrueCounterpart(conversation.getMucOptions()
 					.getTrueCounterpart(counterPart));
@@ -201,10 +196,24 @@ public class MessageParser extends AbstractParser implements
 			return null;
 		}
 		Element message = forwarded.findChild("message");
-		if ((message == null) || (!message.hasChild("body"))) {
+		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;
 		}
@@ -224,8 +233,6 @@ public class MessageParser extends AbstractParser implements
 		String[] parts = fullJid.split("/", 2);
 		Conversation conversation = mXmppConnectionService
 				.findOrCreateConversation(account, parts[0], false);
-		conversation.setLatestMarkableMessageId(getMarkableMessageId(packet));
-
 		String pgpBody = getPgpBody(message);
 		Message finishedMessage;
 		if (pgpBody != null) {
@@ -238,6 +245,7 @@ public class MessageParser extends AbstractParser implements
 		}
 		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);
@@ -298,6 +306,8 @@ public class MessageParser extends AbstractParser implements
 						Element password = x.findChild("password");
 						conversation.getMucOptions().setPassword(
 								password.getContent());
+						mXmppConnectionService.databaseBackend
+								.updateConversation(conversation);
 					}
 					mXmppConnectionService.joinMuc(conversation);
 					mXmppConnectionService.updateConversationUi();
@@ -313,6 +323,8 @@ public class MessageParser extends AbstractParser implements
 				if (!conversation.getMucOptions().online()) {
 					if (password != null) {
 						conversation.getMucOptions().setPassword(password);
+						mXmppConnectionService.databaseBackend
+								.updateConversation(conversation);
 					}
 					mXmppConnectionService.joinMuc(conversation);
 					mXmppConnectionService.updateConversationUi();
@@ -371,22 +383,18 @@ public class MessageParser extends AbstractParser implements
 		}
 	}
 
-	private String getMarkableMessageId(Element message) {
-		if (message.hasChild("markable", "urn:xmpp:chat-markers:0")) {
-			return message.getAttribute("id");
-		} else {
-			return null;
-		}
+	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 = true;
-		if (mXmppConnectionService.getPreferences().getBoolean(
-				"notification_grace_period_after_carbon_received", true)) {
-			notify = (SystemClock.elapsedRealtime() - lastCarbonMessageReceived) > (Config.CARBON_GRACE_PERIOD * 1000);
-		}
+		boolean notify = mXmppConnectionService.getPreferences().getBoolean(
+				"show_notification", true);
+		boolean alwaysNotifyInConference = notify
+				&& mXmppConnectionService.getPreferences().getBoolean(
+						"always_notify_in_conference", false);
 
 		this.parseNick(packet, account);
 
@@ -397,7 +405,9 @@ public class MessageParser extends AbstractParser implements
 				if (message != null) {
 					message.markUnread();
 				}
-			} else if (packet.hasChild("body")) {
+			} else if (packet.hasChild("body")
+					&& !(packet.hasChild("x",
+							"http://jabber.org/protocol/muc#user"))) {
 				message = this.parseChat(packet, account);
 				if (message != null) {
 					message.markUnread();
@@ -407,10 +417,11 @@ public class MessageParser extends AbstractParser implements
 				message = this.parseCarbonMessage(packet, account);
 				if (message != null) {
 					if (message.getStatus() == Message.STATUS_SEND) {
-						lastCarbonMessageReceived = SystemClock
-								.elapsedRealtime();
+						mXmppConnectionService.getNotificationService()
+								.activateGracePeriod();
 						notify = false;
-						message.getConversation().markRead();
+						mXmppConnectionService.markRead(
+								message.getConversation(), false);
 					} else {
 						message.markUnread();
 					}
@@ -423,9 +434,14 @@ public class MessageParser extends AbstractParser implements
 			if (message != null) {
 				if (message.getStatus() == Message.STATUS_RECEIVED) {
 					message.markUnread();
+					notify = alwaysNotifyInConference
+							|| NotificationService
+									.wasHighlightedOrPrivate(message);
 				} else {
-					message.getConversation().markRead();
-					lastCarbonMessageReceived = SystemClock.elapsedRealtime();
+					mXmppConnectionService.markRead(message.getConversation(),
+							false);
+					mXmppConnectionService.getNotificationService()
+							.activateGracePeriod();
 					notify = false;
 				}
 			}
@@ -463,7 +479,10 @@ public class MessageParser extends AbstractParser implements
 			}
 		}
 		notify = notify && !conversation.isMuted();
-		mXmppConnectionService.notifyUi(conversation, notify);
+		if (notify) {
+			mXmppConnectionService.getNotificationService().push(message);
+		}
+		mXmppConnectionService.updateConversationUi();
 	}
 
 	private void parseHeadline(MessagePacket packet, Account account) {

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

@@ -22,7 +22,7 @@ public class PresenceParser extends AbstractParser implements
 		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]);
+					.getAttribute("from").split("/", 2)[0]);
 			if (muc != null) {
 				boolean before = muc.getMucOptions().online();
 				muc.getMucOptions().processPacket(packet, mPgpEngine);
@@ -32,7 +32,7 @@ public class PresenceParser extends AbstractParser implements
 			}
 		} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
 			Conversation muc = mXmppConnectionService.find(account, packet
-					.getAttribute("from").split("/",2)[0]);
+					.getAttribute("from").split("/", 2)[0]);
 			if (muc != null) {
 				boolean before = muc.getMucOptions().online();
 				muc.getMucOptions().processPacket(packet, mPgpEngine);
@@ -58,6 +58,8 @@ public class PresenceParser extends AbstractParser implements
 							Presences.parseShow(packet.findChild("show")));
 				} else if (type.equals("unavailable")) {
 					account.removePresence(fromParts[1]);
+					mXmppConnectionService.getNotificationService()
+							.deactivateGracePeriod();
 				}
 			}
 		} else {

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

@@ -19,7 +19,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 	private static DatabaseBackend instance = null;
 
 	private static final String DATABASE_NAME = "history";
-	private static final int DATABASE_VERSION = 7;
+	private static final int DATABASE_VERSION = 8;
 
 	private static String CREATE_CONTATCS_STATEMENT = "create table "
 			+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -50,10 +50,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 				+ " TEXT, " + Conversation.CONTACT + " TEXT, "
 				+ Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID
 				+ " TEXT, " + Conversation.CREATED + " NUMBER, "
-				+ Conversation.STATUS + " NUMBER," + Conversation.MODE
-				+ " NUMBER," + "FOREIGN KEY(" + Conversation.ACCOUNT
-				+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID
-				+ ") ON DELETE CASCADE);");
+				+ 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
@@ -96,6 +96,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 			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) {
@@ -206,6 +210,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		while (cursor.moveToNext()) {
 			list.add(Account.fromCursor(cursor));
 		}
+		cursor.close();
 		return list;
 	}
 
@@ -221,13 +226,15 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		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);
+		Cursor cursor = db.rawQuery("select count(" + Account.UUID + ")  from "
+				+ Account.TABLENAME + " where not options & (1 <<1)", null);
 		cursor.moveToFirst();
 		int count = cursor.getInt(0);
-		return (count>0);
+		cursor.close();
+		return (count > 0);
 	}
 
 	@Override
@@ -253,6 +260,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		while (cursor.moveToNext()) {
 			roster.initContact(Contact.fromCursor(cursor));
 		}
+		cursor.close();
 	}
 
 	public void writeRoster(Roster roster) {

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

@@ -250,7 +250,10 @@ public class FileBackend {
 			if (!file.exists()) {
 				file = getJingleFileLegacy(message);
 			}
-			Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath());
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			options.inSampleSize = calcSampleSize(file, size);
+			Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
+					options);
 			if (fullsize == null) {
 				throw new FileNotFoundException();
 			}
@@ -414,6 +417,17 @@ public class FileBackend {
 		options.inJustDecodeBounds = true;
 		BitmapFactory.decodeStream(context.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;
@@ -428,7 +442,6 @@ public class FileBackend {
 			}
 		}
 		return inSampleSize;
-
 	}
 
 	public Uri getJingleFileUri(Message message) {

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

@@ -15,7 +15,8 @@ public class EventReceiver extends BroadcastReceiver {
 		} else {
 			mIntentForService.setAction("other");
 		}
-		if (intent.getAction().equals("ui") || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
+		if (intent.getAction().equals("ui")
+				|| DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
 			context.startService(mIntentForService);
 		}
 	}

src/eu/siacs/conversations/services/ImageProvider.java 🔗

@@ -31,7 +31,7 @@ public class ImageProvider extends ContentProvider {
 			if (uuids == null) {
 				throw new FileNotFoundException();
 			}
-			String[] uuidsSplited = uuids.split("/",2);
+			String[] uuidsSplited = uuids.split("/", 2);
 			if (uuidsSplited.length != 3) {
 				throw new FileNotFoundException();
 			}

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

@@ -0,0 +1,241 @@
+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.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.Html;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+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 NotificationManager mNotificationManager;
+
+	private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>();
+
+	public int NOTIFICATION_ID = 0x2342;
+	private Conversation mOpenConversation;
+	private boolean mIsInForeground;
+
+	private long mEndGracePeriod = 0L;
+
+	public NotificationService(XmppConnectionService service) {
+		this.mXmppConnectionService = service;
+		this.mNotificationManager = (NotificationManager) service
+				.getSystemService(Context.NOTIFICATION_SERVICE);
+	}
+
+	public synchronized 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;
+		}
+		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);
+		}
+		updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
+				&& !inGracePeriod());
+	}
+
+	public void clear() {
+		notifications.clear();
+		updateNotification(false);
+	}
+
+	public void clear(Conversation conversation) {
+		notifications.remove(conversation.getUuid());
+		updateNotification(false);
+	}
+
+	private void updateNotification(boolean notify) {
+		SharedPreferences preferences = mXmppConnectionService.getPreferences();
+
+		String ringtone = preferences.getString("notification_ringtone", null);
+		boolean vibrate = preferences.getBoolean("vibrate_on_notification",
+				true);
+
+		if (notifications.size() == 0) {
+			mNotificationManager.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(conversation.getImage(
+							mXmppConnectionService, 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 {
+					mNotificationManager.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());
+			if (!inGracePeriod()) {
+				mBuilder.setLights(0xffffffff, 2000, 4000);
+			}
+			Notification notification = mBuilder.build();
+			mNotificationManager.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;
+	}
+
+	public void activateGracePeriod() {
+		this.mEndGracePeriod = SystemClock.elapsedRealtime()
+				+ (Config.CARBON_GRACE_PERIOD * 1000);
+	}
+
+	public void deactivateGracePeriod() {
+		this.mEndGracePeriod = 0L;
+	}
+
+	private boolean inGracePeriod() {
+		return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
+	}
+}

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

@@ -90,9 +90,12 @@ public class XmppConnectionService extends Service {
 	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;
+
 	private MessageParser mMessageParser = new MessageParser(this);
 	private PresenceParser mPresenceParser = new PresenceParser(this);
 	private IqParser mIqParser = new IqParser(this);
@@ -316,14 +319,16 @@ public class XmppConnectionService extends Service {
 
 	@Override
 	public int onStartCommand(Intent intent, int flags, int startId) {
-		if ((intent != null)
-				&& (ACTION_MERGE_PHONE_CONTACTS.equals(intent.getAction()))) {
-			mergePhoneContactsWithRoster();
-			return START_STICKY;
-		} else if ((intent != null)
-				&& (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))) {
-			logoutAndSave();
-			return START_NOT_STICKY;
+		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();
 		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
@@ -401,6 +406,7 @@ public class XmppConnectionService extends Service {
 		this.mRandom = new SecureRandom();
 		this.mMemorizingTrustManager = new MemorizingTrustManager(
 				getApplicationContext());
+		this.mNotificationService = new NotificationService(this);
 		this.databaseBackend = DatabaseBackend
 				.getInstance(getApplicationContext());
 		this.fileBackend = new FileBackend(getApplicationContext());
@@ -511,7 +517,8 @@ public class XmppConnectionService extends Service {
 		MessagePacket packet = null;
 		boolean saveInDb = true;
 		boolean send = false;
-		if (account.getStatus() == Account.STATUS_ONLINE) {
+		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) {
@@ -561,6 +568,10 @@ public class XmppConnectionService extends Service {
 					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) {
@@ -586,10 +597,6 @@ public class XmppConnectionService extends Service {
 
 		}
 		conv.getMessages().add(message);
-		if (!account.getXmppConnection().getFeatures().sm()
-				&& conv.getMode() != Conversation.MODE_MULTI) {
-			message.setStatus(Message.STATUS_SEND);
-		}
 		if (saveInDb) {
 			if (message.getEncryption() == Message.ENCRYPTION_NONE
 					|| saveEncryptedMessages()) {
@@ -742,7 +749,7 @@ public class XmppConnectionService extends Service {
 		Element query = iqPacket.query("jabber:iq:private");
 		Element storage = query.addChild("storage", "storage:bookmarks");
 		for (Bookmark bookmark : account.getBookmarks()) {
-			storage.addChild(bookmark.toElement());
+			storage.addChild(bookmark);
 		}
 		sendIqPacket(account, iqPacket, null);
 	}
@@ -824,7 +831,7 @@ public class XmppConnectionService extends Service {
 			}
 		});
 	}
-	
+
 	public int loadMoreMessages(Conversation conversation, long timestamp) {
 		List<Message> messages = databaseBackend.getMessages(conversation, 50,
 				timestamp);
@@ -851,8 +858,9 @@ public class XmppConnectionService extends Service {
 	public Conversation find(List<Conversation> haystack, Account account,
 			String jid) {
 		for (Conversation conversation : haystack) {
-			if ((conversation.getAccount().equals(account))
-					&& (conversation.getContactJid().split("/",2)[0].equals(jid))) {
+			if ((account == null || conversation.getAccount().equals(account))
+					&& (conversation.getContactJid().split("/", 2)[0]
+							.equals(jid))) {
 				return conversation;
 			}
 		}
@@ -901,10 +909,12 @@ public class XmppConnectionService extends Service {
 
 	public void archiveConversation(Conversation conversation) {
 		if (conversation.getMode() == Conversation.MODE_MULTI) {
-			Bookmark bookmark = conversation.getBookmark();
-			if (bookmark != null && bookmark.autojoin()) {
-				bookmark.setAutojoin(false);
-				pushBookmarks(bookmark.getAccount());
+			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 {
@@ -963,10 +973,12 @@ public class XmppConnectionService extends Service {
 
 	public void setOnConversationListChangedListener(
 			OnConversationUpdate listener) {
+		this.mNotificationService.deactivateGracePeriod();
 		if (checkListeners()) {
 			switchToForeground();
 		}
 		this.mOnConversationUpdate = listener;
+		this.mNotificationService.setIsInForeground(true);
 		this.convChangedListenerCount++;
 	}
 
@@ -974,6 +986,7 @@ public class XmppConnectionService extends Service {
 		this.convChangedListenerCount--;
 		if (this.convChangedListenerCount == 0) {
 			this.mOnConversationUpdate = null;
+			this.mNotificationService.setIsInForeground(false);
 			if (checkListeners()) {
 				switchToBackground();
 			}
@@ -981,6 +994,7 @@ public class XmppConnectionService extends Service {
 	}
 
 	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
+		this.mNotificationService.deactivateGracePeriod();
 		if (checkListeners()) {
 			switchToForeground();
 		}
@@ -999,6 +1013,7 @@ public class XmppConnectionService extends Service {
 	}
 
 	public void setOnRosterUpdateListener(OnRosterUpdate listener) {
+		this.mNotificationService.deactivateGracePeriod();
 		if (checkListeners()) {
 			switchToForeground();
 		}
@@ -1111,13 +1126,11 @@ public class XmppConnectionService extends Service {
 	public void providePasswordForMuc(Conversation conversation, String password) {
 		if (conversation.getMode() == Conversation.MODE_MULTI) {
 			conversation.getMucOptions().setPassword(password);
-			if (conversation.getBookmark() != null
-					&& conversation.getMucOptions().isPasswordChanged()) {
-				if (!conversation.getBookmark().autojoin()) {
-					conversation.getBookmark().setAutojoin(true);
-				}
+			if (conversation.getBookmark() != null) {
+				conversation.getBookmark().setAutojoin(true);
 				pushBookmarks(conversation.getAccount());
 			}
+			databaseBackend.updateConversation(conversation);
 			joinMuc(conversation);
 		}
 	}
@@ -1266,7 +1279,7 @@ public class XmppConnectionService extends Service {
 				}
 			}
 		}
-		notifyUi(conversation, false);
+		updateConversationUi();
 	}
 
 	public boolean renewSymmetricKey(Conversation conversation) {
@@ -1498,6 +1511,9 @@ public class XmppConnectionService extends Service {
 					thread.start();
 					scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
 							false);
+				} else {
+					account.getRoster().clearPresences();
+					account.setXmppConnection(null);
 				}
 			}
 		}).start();
@@ -1523,24 +1539,34 @@ public class XmppConnectionService extends Service {
 
 	public boolean markMessage(Account account, String recipient, String uuid,
 			int status) {
-		for (Conversation conversation : getConversations()) {
-			if (conversation.getContactJid().equals(recipient)
-					&& conversation.getAccount().equals(account)) {
-				return markMessage(conversation, uuid, 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;
 		}
-		return false;
 	}
 
 	public boolean markMessage(Conversation conversation, String uuid,
 			int status) {
-		for (Message message : conversation.getMessages()) {
-			if (message.getUuid().equals(uuid)) {
-				markMessage(message, status);
-				return true;
+		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;
 		}
-		return false;
 	}
 
 	public void markMessage(Message message, int status) {
@@ -1575,15 +1601,6 @@ public class XmppConnectionService extends Service {
 		return getPreferences().getBoolean("indicate_received", false);
 	}
 
-	public void notifyUi(Conversation conversation, boolean notify) {
-		if (mOnConversationUpdate != null) {
-			mOnConversationUpdate.onConversationUpdate();
-		} else {
-			UIHelper.updateNotification(getApplicationContext(),
-					getConversations(), conversation, notify);
-		}
-	}
-
 	public void updateConversationUi() {
 		if (mOnConversationUpdate != null) {
 			mOnConversationUpdate.onConversationUpdate();
@@ -1620,15 +1637,21 @@ public class XmppConnectionService extends Service {
 		return null;
 	}
 
-	public void markRead(Conversation conversation) {
+	public void markRead(Conversation conversation, boolean calledByUi) {
+		mNotificationService.clear(conversation);
+		String id = conversation.getLatestMarkableMessageId();
 		conversation.markRead();
-		String id = conversation.popLatestMarkableMessageId();
-		if (confirmMessages() && id != null) {
+		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) {
@@ -1703,16 +1726,25 @@ public class XmppConnectionService extends Service {
 	}
 
 	public void sendMessagePacket(Account account, MessagePacket packet) {
-		account.getXmppConnection().sendMessagePacket(packet);
+		XmppConnection connection = account.getXmppConnection();
+		if (connection != null) {
+			connection.sendMessagePacket(packet);
+		}
 	}
 
 	public void sendPresencePacket(Account account, PresencePacket packet) {
-		account.getXmppConnection().sendPresencePacket(packet);
+		XmppConnection connection = account.getXmppConnection();
+		if (connection != null) {
+			connection.sendPresencePacket(packet);
+		}
 	}
 
 	public void sendIqPacket(Account account, IqPacket packet,
 			OnIqPacketReceived callback) {
-		account.getXmppConnection().sendIqPacket(packet, callback);
+		XmppConnection connection = account.getXmppConnection();
+		if (connection != null) {
+			connection.sendIqPacket(packet, callback);
+		}
 	}
 
 	public MessageGenerator getMessageGenerator() {
@@ -1742,4 +1774,22 @@ public class XmppConnectionService extends Service {
 	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()
+						.getContactAsShownInRoster(jid);
+				if (contact != null) {
+					contacts.add(contact);
+				}
+			}
+		}
+		return contacts;
+	}
+
+	public NotificationService getNotificationService() {
+		return this.mNotificationService;
+	}
 }

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

@@ -201,7 +201,7 @@ public class ConferenceDetailsActivity extends XmppActivity {
 	private void populateView() {
 		mYourPhoto.setImageBitmap(conversation.getAccount().getImage(this, 48));
 		setTitle(conversation.getName());
-		mFullJid.setText(conversation.getContactJid().split("/",2)[0]);
+		mFullJid.setText(conversation.getContactJid().split("/", 2)[0]);
 		mYourNick.setText(conversation.getMucOptions().getActualNick());
 		mRoleAffiliaton = (TextView) findViewById(R.id.muc_role);
 		if (conversation.getMucOptions().online()) {

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

@@ -39,8 +39,6 @@ import eu.siacs.conversations.utils.UIHelper;
 public class ContactDetailsActivity extends XmppActivity {
 	public static final String ACTION_VIEW_CONTACT = "view_contact";
 
-	protected ContactDetailsActivity activity = this;
-
 	private Contact contact;
 
 	private String accountJid;
@@ -58,8 +56,8 @@ public class ContactDetailsActivity extends XmppActivity {
 
 		@Override
 		public void onClick(DialogInterface dialog, int which) {
-			activity.xmppConnectionService.deleteContactOnServer(contact);
-			activity.finish();
+			ContactDetailsActivity.this.xmppConnectionService.deleteContactOnServer(contact);
+			ContactDetailsActivity.this.finish();
 		}
 	};
 
@@ -73,14 +71,14 @@ public class ContactDetailsActivity extends XmppActivity {
 			intent.putExtra(Intents.Insert.IM_PROTOCOL,
 					CommonDataKinds.Im.PROTOCOL_JABBER);
 			intent.putExtra("finishActivityOnSaveCompleted", true);
-			activity.startActivityForResult(intent, 0);
+			ContactDetailsActivity.this.startActivityForResult(intent, 0);
 		}
 	};
 	private OnClickListener onBadgeClick = new OnClickListener() {
 
 		@Override
 		public void onClick(View v) {
-			AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+			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()));
@@ -206,7 +204,7 @@ public class ContactDetailsActivity extends XmppActivity {
 					@Override
 					public void onValueEdited(String value) {
 						contact.setServerName(value);
-						activity.xmppConnectionService
+						ContactDetailsActivity.this.xmppConnectionService
 								.pushContactToServer(contact);
 						populateView();
 					}
@@ -354,7 +352,7 @@ public class ContactDetailsActivity extends XmppActivity {
 
 				@Override
 				public void onClick(View v) {
-					PgpEngine pgp = activity.xmppConnectionService
+					PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService
 							.getPgpEngine();
 					if (pgp != null) {
 						PendingIntent intent = pgp.getIntentForKey(contact);

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

@@ -12,11 +12,11 @@ import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdat
 import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
 import eu.siacs.conversations.ui.adapter.ConversationAdapter;
 import eu.siacs.conversations.utils.ExceptionHelper;
-import eu.siacs.conversations.utils.UIHelper;
 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;
@@ -59,8 +59,13 @@ public class ConversationActivity extends XmppActivity implements
 	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";
 
-	protected SlidingPaneLayout spl;
+	private String mOpenConverstaion = null;
+	private boolean mPanelOpen = true;
+
+	private View mContentView;
 
 	private List<Conversation> conversationList = new ArrayList<Conversation>();
 	private Conversation selectedConversation = null;
@@ -69,7 +74,6 @@ public class ConversationActivity extends XmppActivity implements
 	private boolean paneShouldBeOpen = true;
 	private ArrayAdapter<Conversation> listAdapter;
 
-	protected ConversationActivity activity = this;
 	private Toast prepareImageToast;
 
 	private Uri pendingImageUri = null;
@@ -90,18 +94,52 @@ public class ConversationActivity extends XmppActivity implements
 		return this.listView;
 	}
 
-	public SlidingPaneLayout getSlidingPaneLayout() {
-		return this.spl;
-	}
-
 	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);
@@ -122,63 +160,80 @@ public class ConversationActivity extends XmppActivity implements
 					setSelectedConversation(conversationList.get(position));
 					swapConversationFragment();
 				} else {
-					spl.closePane();
+					hideConversationsOverview();
 				}
 			}
 		});
-		spl = (SlidingPaneLayout) findViewById(R.id.slidingpanelayout);
-		spl.setParallaxDistance(150);
-		spl.setShadowResource(R.drawable.es_slidingpane_shadow);
-		spl.setSliderFadeColor(0);
-		spl.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();
-			}
+		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 onPanelClosed(View arg0) {
-				paneShouldBeOpen = false;
-				if ((conversationList.size() > 0)
-						&& (getSelectedConversation() != null)) {
+				@Override
+				public void onPanelOpened(View arg0) {
+					paneShouldBeOpen = true;
 					ActionBar ab = getActionBar();
 					if (ab != null) {
-						ab.setDisplayHomeAsUpEnabled(true);
-						ab.setHomeButtonEnabled(true);
-						if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE
-								|| activity.useSubjectToIdentifyConference()) {
-							ab.setTitle(getSelectedConversation().getName());
-						} else {
-							ab.setTitle(getSelectedConversation()
-									.getContactJid().split("/")[0]);
-						}
+						ab.setDisplayHomeAsUpEnabled(false);
+						ab.setHomeButtonEnabled(false);
+						ab.setTitle(R.string.app_name);
 					}
 					invalidateOptionsMenu();
-					if (!getSelectedConversation().isRead()) {
-						xmppConnectionService
-								.markRead(getSelectedConversation());
-						UIHelper.updateNotification(getApplicationContext(),
-								getConversationList(), null, false);
-						listView.invalidateViews();
+					hideKeyboard();
+					if (xmppConnectionServiceBound) {
+						xmppConnectionService.getNotificationService()
+								.setOpenConversation(null);
 					}
 				}
-			}
 
-			@Override
-			public void onPanelSlide(View arg0, float arg1) {
-				// TODO Auto-generated method stub
+				@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
@@ -198,7 +253,8 @@ public class ConversationActivity extends XmppActivity implements
 				.findItem(R.id.action_invite);
 		MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute);
 
-		if ((spl.isOpen() && (spl.isSlideable()))) {
+		if (isConversationsOverviewVisable()
+				&& isConversationsOverviewHideable()) {
 			menuArchive.setVisible(false);
 			menuMucDetails.setVisible(false);
 			menuContactDetails.setVisible(false);
@@ -208,7 +264,7 @@ public class ConversationActivity extends XmppActivity implements
 			menuClearHistory.setVisible(false);
 			menuMute.setVisible(false);
 		} else {
-			menuAdd.setVisible(!spl.isSlideable());
+			menuAdd.setVisible(!isConversationsOverviewHideable());
 			if (this.getSelectedConversation() != null) {
 				if (this.getSelectedConversation().getLatestMessage()
 						.getEncryption() != Message.ENCRYPTION_NONE) {
@@ -296,6 +352,8 @@ public class ConversationActivity extends XmppActivity implements
 											int which) {
 										conversation
 												.setNextEncryption(Message.ENCRYPTION_NONE);
+										xmppConnectionService.databaseBackend
+												.updateConversation(conversation);
 										selectPresenceToAttachFile(attachmentChoice);
 									}
 								});
@@ -315,7 +373,7 @@ public class ConversationActivity extends XmppActivity implements
 	@Override
 	public boolean onOptionsItemSelected(MenuItem item) {
 		if (item.getItemId() == android.R.id.home) {
-			spl.openPane();
+			showConversationsOverview();
 			return true;
 		} else if (item.getItemId() == R.id.action_add) {
 			startActivity(new Intent(this, StartConversationActivity.class));
@@ -367,7 +425,7 @@ public class ConversationActivity extends XmppActivity implements
 	public void endConversation(Conversation conversation) {
 		conversation.setStatus(Conversation.STATUS_ARCHIVED);
 		paneShouldBeOpen = true;
-		spl.openPane();
+		showConversationsOverview();
 		xmppConnectionService.archiveConversation(conversation);
 		if (conversationList.size() > 0) {
 			setSelectedConversation(conversationList.get(0));
@@ -376,6 +434,7 @@ public class ConversationActivity extends XmppActivity implements
 		}
 	}
 
+	@SuppressLint("InflateParams")
 	protected void clearHistoryDialog(final Conversation conversation) {
 		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 		builder.setTitle(getString(R.string.clear_conversation_history));
@@ -390,7 +449,7 @@ public class ConversationActivity extends XmppActivity implements
 
 					@Override
 					public void onClick(DialogInterface dialog, int which) {
-						activity.xmppConnectionService
+						ConversationActivity.this.xmppConnectionService
 								.clearConversationHistory(conversation);
 						if (endConversationCheckBox.isChecked()) {
 							endConversation(conversation);
@@ -399,7 +458,7 @@ public class ConversationActivity extends XmppActivity implements
 				});
 		builder.create().show();
 	}
-	
+
 	protected void attachFileDialog() {
 		View menuAttachFile = findViewById(R.id.action_attach_file);
 		if (menuAttachFile == null) {
@@ -470,6 +529,8 @@ public class ConversationActivity extends XmppActivity implements
 						conversation.setNextEncryption(Message.ENCRYPTION_NONE);
 						break;
 					}
+					xmppConnectionService.databaseBackend
+							.updateConversation(conversation);
 					fragment.updateChatMsgHint();
 					return true;
 				}
@@ -523,6 +584,8 @@ public class ConversationActivity extends XmppActivity implements
 									+ (durations[which] * 1000);
 						}
 						conversation.setMutedTill(till);
+						ConversationActivity.this.xmppConnectionService.databaseBackend
+								.updateConversation(conversation);
 						ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
 								.findFragmentByTag("conversation");
 						if (selectedFragment != null) {
@@ -550,8 +613,8 @@ public class ConversationActivity extends XmppActivity implements
 	@Override
 	public boolean onKeyDown(int keyCode, KeyEvent event) {
 		if (keyCode == KeyEvent.KEYCODE_BACK) {
-			if (!spl.isOpen()) {
-				spl.openPane();
+			if (!isConversationsOverviewVisable()) {
+				showConversationsOverview();
 				return false;
 			}
 		}
@@ -599,62 +662,75 @@ public class ConversationActivity extends XmppActivity implements
 			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();
-		if (conversationList.size() == 0) {
-			updateConversationList();
+		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 (getSelectedConversation() != null && pendingImageUri != null) {
+		if (pendingImageUri != null) {
 			attachImageToConversation(getSelectedConversation(),
 					pendingImageUri);
 			pendingImageUri = null;
-		} else {
-			pendingImageUri = null;
 		}
+		ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
+	}
 
-		if ((getIntent().getAction() != null)
-				&& (getIntent().getAction().equals(Intent.ACTION_VIEW) && (!handledViewIntent))) {
-			if (getIntent().getType().equals(
-					ConversationActivity.VIEW_CONVERSATION)) {
-				handledViewIntent = true;
-
-				String convToView = (String) getIntent().getExtras().get(
-						CONVERSATION);
-
-				for (int i = 0; i < conversationList.size(); ++i) {
-					if (conversationList.get(i).getUuid().equals(convToView)) {
-						setSelectedConversation(conversationList.get(i));
-					}
-				}
-				paneShouldBeOpen = false;
-				String text = getIntent().getExtras().getString(TEXT, null);
-				swapConversationFragment().setText(text);
-			}
-		} else {
-			if (xmppConnectionService.getAccounts().size() == 0) {
-				startActivity(new Intent(this, EditAccountActivity.class));
-			} else if (conversationList.size() <= 0) {
-				// add no history
-				startActivity(new Intent(this, StartConversationActivity.class));
-				finish();
-			} else {
-				spl.openPane();
-				// find currently loaded fragment
-				ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
-						.findFragmentByTag("conversation");
-				if (selectedFragment != null) {
-					selectedFragment.onBackendConnected();
-				} else {
-					setSelectedConversation(conversationList.get(0));
-					swapConversationFragment();
-				}
-				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));
 			}
 		}
 	}
@@ -778,7 +854,7 @@ public class ConversationActivity extends XmppActivity implements
 					@Override
 					public void userInputRequried(PendingIntent pi,
 							Message message) {
-						activity.runIntent(pi,
+						ConversationActivity.this.runIntent(pi,
 								ConversationActivity.REQUEST_SEND_MESSAGE);
 					}
 

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

@@ -76,10 +76,11 @@ public class ConversationFragment extends Fragment {
 
 		@Override
 		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-			if (actionId == EditorInfo.IME_ACTION_DONE) {
+			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;
@@ -131,6 +132,14 @@ public class ConversationFragment extends Fragment {
 		}
 	};
 
+	private OnClickListener joinMuc = new OnClickListener() {
+
+		@Override
+		public void onClick(View v) {
+			activity.xmppConnectionService.joinMuc(conversation);
+		}
+	};
+
 	private OnClickListener enterPassword = new OnClickListener() {
 
 		@Override
@@ -169,6 +178,7 @@ public class ConversationFragment extends Fragment {
 						conversation, timestamp);
 				messageList.clear();
 				messageList.addAll(conversation.getMessages());
+				updateStatusMessages();
 				messageListAdapter.notifyDataSetChanged();
 				if (size != 0) {
 					messagesLoaded = true;
@@ -244,9 +254,7 @@ public class ConversationFragment extends Fragment {
 
 			@Override
 			public void onClick(View v) {
-				if (activity.getSlidingPaneLayout().isSlideable()) {
-					activity.getSlidingPaneLayout().closePane();
-				}
+				activity.hideConversationsOverview();
 			}
 		});
 		mEditMessage.setOnEditorActionListener(mEditorActionListener);
@@ -375,17 +383,10 @@ public class ConversationFragment extends Fragment {
 		int position = mEditMessage.length();
 		Editable etext = mEditMessage.getText();
 		Selection.setSelection(etext, position);
-		if (activity.getSlidingPaneLayout().isSlideable()) {
+		if (activity.isConversationsOverviewHideable()) {
 			if (!activity.shouldPaneBeOpen()) {
-				activity.getSlidingPaneLayout().closePane();
-				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
-				activity.getActionBar().setHomeButtonEnabled(true);
-				if (conversation.getMode() == Conversation.MODE_SINGLE || activity.useSubjectToIdentifyConference()) {
-					activity.getActionBar().setTitle(conversation.getName());
-				} else {
-					activity.getActionBar().setTitle(conversation.getContactJid().split("/")[0]);
-				}
-				activity.invalidateOptionsMenu();
+				activity.hideConversationsOverview();
+				activity.openConversation(conversation);
 			}
 		}
 		if (this.conversation.getMode() == Conversation.MODE_MULTI) {
@@ -437,6 +438,8 @@ public class ConversationFragment extends Fragment {
 							@Override
 							public void onClick(View v) {
 								conversation.setMutedTill(0);
+								activity.xmppConnectionService.databaseBackend
+										.updateConversation(conversation);
 								updateMessages();
 							}
 						});
@@ -492,6 +495,18 @@ public class ConversationFragment extends Fragment {
 						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;
 					}
@@ -500,9 +515,7 @@ public class ConversationFragment extends Fragment {
 			getActivity().invalidateOptionsMenu();
 			updateChatMsgHint();
 			if (!activity.shouldPaneBeOpen()) {
-				activity.xmppConnectionService.markRead(conversation);
-				UIHelper.updateNotification(getActivity(),
-						activity.getConversationList(), null, false);
+				activity.xmppConnectionService.markRead(conversation, true);
 				activity.updateConversationList();
 			}
 			this.updateSendButton();
@@ -511,9 +524,7 @@ public class ConversationFragment extends Fragment {
 
 	private void messageSent() {
 		int size = this.messageList.size();
-		if (size >= 1 && this.messagesView.getLastVisiblePosition() != size - 1) {
-			messagesView.setSelection(size - 1);
-		}
+		messagesView.setSelection(size - 1);
 		mEditMessage.setText("");
 		updateChatMsgHint();
 	}
@@ -667,6 +678,8 @@ public class ConversationFragment extends Fragment {
 										int which) {
 									conversation
 											.setNextEncryption(Message.ENCRYPTION_NONE);
+									xmppService.databaseBackend
+											.updateConversation(conversation);
 									message.setEncryption(Message.ENCRYPTION_NONE);
 									xmppService.sendMessage(message);
 									messageSent();
@@ -695,6 +708,8 @@ public class ConversationFragment extends Fragment {
 									conversation
 											.setNextEncryption(Message.ENCRYPTION_NONE);
 									message.setEncryption(Message.ENCRYPTION_NONE);
+									xmppService.databaseBackend
+											.updateConversation(conversation);
 									xmppService.sendMessage(message);
 									messageSent();
 								}

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

@@ -1,8 +1,12 @@
 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;
@@ -10,9 +14,11 @@ 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.TextView;
+import android.widget.Toast;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@@ -38,6 +44,7 @@ public class EditAccountActivity extends XmppActivity {
 	private TextView mSessionEst;
 	private TextView mOtrFingerprint;
 	private TextView mOtrFingerprintHeadline;
+	private ImageButton mOtrFingerprintToClipboardButton;
 
 	private String jidToEdit;
 	private Account mAccount;
@@ -48,6 +55,12 @@ public class EditAccountActivity extends XmppActivity {
 
 		@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();
@@ -157,6 +170,25 @@ public class EditAccountActivity extends XmppActivity {
 		}
 	};
 	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() {
@@ -197,6 +229,11 @@ public class EditAccountActivity extends XmppActivity {
 			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());
@@ -204,6 +241,10 @@ public class EditAccountActivity extends XmppActivity {
 				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);
 				}
@@ -213,12 +254,21 @@ public class EditAccountActivity extends XmppActivity {
 		}
 	}
 
+	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);
@@ -228,6 +278,7 @@ public class EditAccountActivity extends XmppActivity {
 		this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
 		this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
 		this.mOtrFingerprintHeadline = (TextView) findViewById(R.id.otr_fingerprint_headline);
+		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);
@@ -255,7 +306,7 @@ public class EditAccountActivity extends XmppActivity {
 			this.jidToEdit = getIntent().getStringExtra("jid");
 			if (this.jidToEdit != null) {
 				this.mRegisterNew.setVisibility(View.GONE);
-				getActionBar().setTitle(R.string.mgmt_account_edit);
+				getActionBar().setTitle(jidToEdit);
 			} else {
 				getActionBar().setTitle(R.string.action_add_account);
 			}
@@ -324,13 +375,30 @@ public class EditAccountActivity extends XmppActivity {
 			} else {
 				this.mServerInfoPep.setText(R.string.server_info_unavailable);
 			}
-			String fingerprint = this.mAccount
+			final String fingerprint = this.mAccount
 					.getOtrFingerprint(xmppConnectionService);
 			if (fingerprint != null) {
 				this.mOtrFingerprintHeadline.setVisibility(View.VISIBLE);
 				this.mOtrFingerprint.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.mOtrFingerprintToClipboardButton.setVisibility(View.GONE);
 				this.mOtrFingerprint.setVisibility(View.GONE);
 				this.mOtrFingerprintHeadline.setVisibility(View.GONE);
 			}
@@ -343,4 +411,15 @@ public class EditAccountActivity extends XmppActivity {
 			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;
+	}
 }

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

@@ -24,8 +24,6 @@ import android.widget.ListView;
 
 public class ManageAccountActivity extends XmppActivity {
 
-	protected ManageAccountActivity activity = this;
-
 	protected Account selectedAccount = null;
 
 	protected List<Account> accountList = new ArrayList<Account>();
@@ -72,7 +70,7 @@ public class ManageAccountActivity extends XmppActivity {
 	public void onCreateContextMenu(ContextMenu menu, View v,
 			ContextMenuInfo menuInfo) {
 		super.onCreateContextMenu(menu, v, menuInfo);
-		activity.getMenuInflater().inflate(R.menu.manageaccounts_context, menu);
+		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)) {
@@ -181,7 +179,7 @@ public class ManageAccountActivity extends XmppActivity {
 	}
 
 	private void publishOpenPGPPublicKey(Account account) {
-		if (activity.hasPgp()) {
+		if (ManageAccountActivity.this.hasPgp()) {
 			announcePgp(account, null);
 		} else {
 			this.showInstallPgpDialog();
@@ -189,7 +187,7 @@ public class ManageAccountActivity extends XmppActivity {
 	}
 
 	private void deleteAccount(final Account account) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+		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));

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

@@ -21,7 +21,7 @@ public class SettingsActivity extends XmppActivity implements
 		super.onCreate(savedInstanceState);
 		mSettingsFragment = new SettingsFragment();
 		getFragmentManager().beginTransaction()
-				.replace(android.R.id.content,mSettingsFragment).commit();
+				.replace(android.R.id.content, mSettingsFragment).commit();
 	}
 
 	@Override
@@ -34,12 +34,16 @@ public class SettingsActivity extends XmppActivity implements
 		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()]));
+		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()]));
 		}
 	}
 

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

@@ -1,9 +1,12 @@
 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;
@@ -14,6 +17,8 @@ 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;
@@ -157,6 +162,8 @@ public class StartConversationActivity extends XmppActivity {
 			});
 		}
 	};
+	private MenuItem mMenuSearchView;
+	private String mInitialJid;
 
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
@@ -308,7 +315,8 @@ public class StartConversationActivity extends XmppActivity {
 
 	}
 
-	protected void showCreateContactDialog() {
+	@SuppressLint("InflateParams")
+	protected void showCreateContactDialog(String prefilledJid) {
 		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 		builder.setTitle(R.string.create_contact);
 		View dialogView = getLayoutInflater().inflate(
@@ -318,6 +326,9 @@ public class StartConversationActivity extends XmppActivity {
 				.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);
@@ -348,8 +359,8 @@ public class StartConversationActivity extends XmppActivity {
 								jid.setError(getString(R.string.contact_already_exists));
 							} else {
 								xmppConnectionService.createContact(contact);
-								switchToConversation(contact);
 								dialog.dismiss();
+								switchToConversation(contact);
 							}
 						} else {
 							jid.setError(getString(R.string.invalid_jid));
@@ -359,6 +370,7 @@ public class StartConversationActivity extends XmppActivity {
 
 	}
 
+	@SuppressLint("InflateParams")
 	protected void showJoinConferenceDialog() {
 		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 		builder.setTitle(R.string.join_conference);
@@ -391,6 +403,10 @@ public class StartConversationActivity extends XmppActivity {
 							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));
@@ -409,6 +425,7 @@ public class StartConversationActivity extends XmppActivity {
 										xmppConnectionService
 												.joinMuc(conversation);
 									}
+									dialog.dismiss();
 									switchToConversation(conversation);
 								}
 							} else {
@@ -418,6 +435,7 @@ public class StartConversationActivity extends XmppActivity {
 								if (!conversation.getMucOptions().online()) {
 									xmppConnectionService.joinMuc(conversation);
 								}
+								dialog.dismiss();
 								switchToConversation(conversation);
 							}
 						} else {
@@ -449,9 +467,9 @@ public class StartConversationActivity extends XmppActivity {
 				.findItem(R.id.action_create_contact);
 		MenuItem menuCreateConference = (MenuItem) menu
 				.findItem(R.id.action_join_conference);
-		MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search);
-		menuSearchView.setOnActionExpandListener(mOnActionExpandListener);
-		View mSearchView = menuSearchView.getActionView();
+		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);
@@ -460,6 +478,11 @@ public class StartConversationActivity extends XmppActivity {
 		} else {
 			menuCreateContact.setVisible(false);
 		}
+		if (mInitialJid != null) {
+			mMenuSearchView.expandActionView();
+			mSearchEditText.append(mInitialJid);
+			filter(mInitialJid);
+		}
 		return true;
 	}
 
@@ -467,7 +490,7 @@ public class StartConversationActivity extends XmppActivity {
 	public boolean onOptionsItemSelected(MenuItem item) {
 		switch (item.getItemId()) {
 		case R.id.action_create_contact:
-			showCreateContactDialog();
+			showCreateContactDialog(null);
 			break;
 		case R.id.action_join_conference:
 			showJoinConferenceDialog();
@@ -486,13 +509,8 @@ public class StartConversationActivity extends XmppActivity {
 	}
 
 	@Override
-	void onBackendConnected() {
+	protected void onBackendConnected() {
 		xmppConnectionService.setOnRosterUpdateListener(this.onRosterUpdate);
-		if (mSearchEditText != null) {
-			filter(mSearchEditText.getText().toString());
-		} else {
-			filter(null);
-		}
 		this.mActivatedAccounts.clear();
 		for (Account account : xmppConnectionService.getAccounts()) {
 			if (account.getStatus() != Account.STATUS_DISABLED) {
@@ -502,6 +520,55 @@ public class StartConversationActivity extends XmppActivity {
 		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) {

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

@@ -15,6 +15,7 @@ import eu.siacs.conversations.entities.Presences;
 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;
@@ -58,11 +59,12 @@ public abstract class XmppActivity extends Activity {
 
 	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;
@@ -206,6 +208,8 @@ public abstract class XmppActivity extends Activity {
 		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);
 		}
@@ -216,7 +220,7 @@ public abstract class XmppActivity extends Activity {
 		return PreferenceManager
 				.getDefaultSharedPreferences(getApplicationContext());
 	}
-	
+
 	public boolean useSubjectToIdentifyConference() {
 		return mUseSubject;
 	}
@@ -245,6 +249,7 @@ public abstract class XmppActivity extends Activity {
 					| Intent.FLAG_ACTIVITY_CLEAR_TOP);
 		}
 		startActivity(viewConversationIntent);
+		finish();
 	}
 
 	public void switchToContactDetails(Contact contact) {
@@ -254,7 +259,7 @@ public abstract class XmppActivity extends Activity {
 		intent.putExtra("contact", contact.getJid());
 		startActivity(intent);
 	}
-	
+
 	public void switchToAccount(Account account) {
 		Intent intent = new Intent(this, EditAccountActivity.class);
 		intent.putExtra("jid", account.getJid());
@@ -292,6 +297,8 @@ public abstract class XmppActivity extends Activity {
 						if (conversation != null) {
 							conversation
 									.setNextEncryption(Message.ENCRYPTION_PGP);
+							xmppConnectionService.databaseBackend
+									.updateConversation(conversation);
 						}
 					}
 
@@ -389,6 +396,7 @@ public abstract class XmppActivity extends Activity {
 		quickEdit(previousValue, callback, true);
 	}
 
+	@SuppressLint("InflateParams")
 	private void quickEdit(final String previousValue,
 			final OnValueEdited callback, boolean password) {
 		AlertDialog.Builder builder = new AlertDialog.Builder(this);
@@ -514,6 +522,10 @@ public abstract class XmppActivity extends Activity {
 		return this.mPrimaryColor;
 	}
 
+	public int getSecondaryBackgroundColor() {
+		return this.mSecondaryBackgroundColor;
+	}
+
 	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
 		private final WeakReference<ImageView> imageViewReference;
 		private Message message = null;

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

@@ -40,9 +40,10 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
 		Conversation conv = getItem(position);
 		if (this.activity instanceof ConversationActivity) {
 			ConversationActivity activity = (ConversationActivity) this.activity;
-			if (!activity.getSlidingPaneLayout().isSlideable()) {
+			if (!activity.isConversationsOverviewHideable()) {
 				if (conv == activity.getSelectedConversation()) {
-					view.setBackgroundColor(0xffdddddd);
+					view.setBackgroundColor(activity
+							.getSecondaryBackgroundColor());
 				} else {
 					view.setBackgroundColor(Color.TRANSPARENT);
 				}
@@ -52,7 +53,8 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
 		}
 		TextView convName = (TextView) view
 				.findViewById(R.id.conversation_name);
-		if (conv.getMode() == Conversation.MODE_SINGLE || activity.useSubjectToIdentifyConference()) {
+		if (conv.getMode() == Conversation.MODE_SINGLE
+				|| activity.useSubjectToIdentifyConference()) {
 			convName.setText(conv.getName());
 		} else {
 			convName.setText(conv.getContactJid().split("/")[0]);

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

@@ -24,7 +24,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
 				.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 		ListItem item = getItem(position);
 		if (view == null) {
-			view = (View) inflater.inflate(R.layout.contact, 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);
@@ -36,4 +36,4 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
 		return view;
 	}
 
-}
+}

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

@@ -417,7 +417,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 			viewHolder = (ViewHolder) view.getTag();
 		}
 
-		if (type == STATUS || type == NULL) {
+		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;
 		}
 

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

@@ -30,17 +30,17 @@ public class DNSHelper {
 		String dns[] = client.findDNS();
 
 		if (dns != null) {
-			// we have a list of DNS servers, let's go
 			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;
 				}
 			}
 		}
-
-		// fallback
 		return queryDNS(host, InetAddress.getByName("8.8.8.8"));
 	}
 
@@ -164,10 +164,8 @@ public class DNSHelper {
 			}
 
 		} catch (SocketTimeoutException e) {
-			Log.d(Config.LOGTAG, "timeout during dns");
 			namePort.putString("error", "timeout");
 		} catch (Exception e) {
-			Log.d(Config.LOGTAG, "unhandled exception in sub project");
 			namePort.putString("error", "unhandled");
 		}
 		return namePort;

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

@@ -7,16 +7,15 @@ import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Pattern;
-import java.util.regex.Matcher;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions.User;
 import eu.siacs.conversations.ui.ConversationActivity;
 import eu.siacs.conversations.ui.ManageAccountActivity;
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Notification;
@@ -26,7 +25,6 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
@@ -34,13 +32,11 @@ import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.net.Uri;
-import android.preference.PreferenceManager;
 import android.provider.ContactsContract.Contacts;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.TaskStackBuilder;
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
-import android.text.Html;
 import android.util.DisplayMetrics;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -330,177 +326,6 @@ public class UIHelper {
 		mNotificationManager.notify(1111, notification);
 	}
 
-	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 static void updateNotification(Context context,
-			List<Conversation> conversations, Conversation currentCon,
-			boolean notify) {
-		NotificationManager mNotificationManager = (NotificationManager) context
-				.getSystemService(Context.NOTIFICATION_SERVICE);
-
-		SharedPreferences preferences = PreferenceManager
-				.getDefaultSharedPreferences(context);
-		boolean showNofifications = preferences.getBoolean("show_notification",
-				true);
-		boolean vibrate = preferences.getBoolean("vibrate_on_notification",
-				true);
-		boolean alwaysNotify = preferences.getBoolean(
-				"notify_in_conversation_when_highlighted", false);
-
-		if (!showNofifications) {
-			mNotificationManager.cancel(2342);
-			return;
-		}
-
-		String targetUuid = "";
-
-		if ((currentCon != null)
-				&& (currentCon.getMode() == Conversation.MODE_MULTI)
-				&& (!alwaysNotify) && notify) {
-			String nick = currentCon.getMucOptions().getActualNick();
-			Pattern highlight = generateNickHighlightPattern(nick);
-			Matcher m = highlight.matcher(currentCon.getLatestMessage()
-					.getBody());
-			notify = m.find()
-					|| (currentCon.getLatestMessage().getType() == Message.TYPE_PRIVATE);
-		}
-
-		List<Conversation> unread = new ArrayList<Conversation>();
-		for (Conversation conversation : conversations) {
-			if (conversation.getMode() == Conversation.MODE_MULTI) {
-				if ((!conversation.isRead())
-						&& ((wasHighlightedOrPrivate(conversation) || (alwaysNotify)))) {
-					unread.add(conversation);
-				}
-			} else {
-				if (!conversation.isRead()) {
-					unread.add(conversation);
-				}
-			}
-		}
-		String ringtone = preferences.getString("notification_ringtone", null);
-
-		NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
-				context);
-		if (unread.size() == 0) {
-			mNotificationManager.cancel(2342);
-			return;
-		} else if (unread.size() == 1) {
-			Conversation conversation = unread.get(0);
-			targetUuid = conversation.getUuid();
-			mBuilder.setLargeIcon(conversation.getImage(context, 64));
-			mBuilder.setContentTitle(conversation.getName());
-			if (notify) {
-				mBuilder.setTicker(conversation.getLatestMessage()
-						.getReadableBody(context));
-			}
-			StringBuilder bigText = new StringBuilder();
-			List<Message> messages = conversation.getMessages();
-			String firstLine = "";
-			for (int i = messages.size() - 1; i >= 0; --i) {
-				if (!messages.get(i).isRead()) {
-					if (i == messages.size() - 1) {
-						firstLine = messages.get(i).getReadableBody(context);
-						bigText.append(firstLine);
-					} else {
-						firstLine = messages.get(i).getReadableBody(context);
-						bigText.insert(0, firstLine + "\n");
-					}
-				} else {
-					break;
-				}
-			}
-			mBuilder.setContentText(firstLine);
-			mBuilder.setStyle(new NotificationCompat.BigTextStyle()
-					.bigText(bigText.toString()));
-		} else {
-			NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
-			style.setBigContentTitle(unread.size() + " "
-					+ context.getString(R.string.unread_conversations));
-			StringBuilder names = new StringBuilder();
-			for (int i = 0; i < unread.size(); ++i) {
-				targetUuid = unread.get(i).getUuid();
-				if (i < unread.size() - 1) {
-					names.append(unread.get(i).getName() + ", ");
-				} else {
-					names.append(unread.get(i).getName());
-				}
-				style.addLine(Html.fromHtml("<b>"
-						+ unread.get(i).getName()
-						+ "</b> "
-						+ unread.get(i).getLatestMessage()
-								.getReadableBody(context)));
-			}
-			mBuilder.setContentTitle(unread.size() + " "
-					+ context.getString(R.string.unread_conversations));
-			mBuilder.setContentText(names.toString());
-			mBuilder.setStyle(style);
-		}
-		if ((currentCon != null) && (notify)) {
-			targetUuid = currentCon.getUuid();
-		}
-		if (unread.size() != 0) {
-			mBuilder.setSmallIcon(R.drawable.ic_notification);
-			if (notify) {
-				if (vibrate) {
-					int dat = 70;
-					long[] pattern = { 0, 3 * dat, dat, dat };
-					mBuilder.setVibrate(pattern);
-				}
-				mBuilder.setLights(0xffffffff, 2000, 4000);
-				if (ringtone != null) {
-					mBuilder.setSound(Uri.parse(ringtone));
-				}
-			}
-
-			TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
-			stackBuilder.addParentStack(ConversationActivity.class);
-
-			Intent viewConversationIntent = new Intent(context,
-					ConversationActivity.class);
-			viewConversationIntent.setAction(Intent.ACTION_VIEW);
-			viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
-					targetUuid);
-			viewConversationIntent
-					.setType(ConversationActivity.VIEW_CONVERSATION);
-
-			stackBuilder.addNextIntent(viewConversationIntent);
-
-			PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(
-					0, PendingIntent.FLAG_UPDATE_CURRENT);
-
-			mBuilder.setContentIntent(resultPendingIntent);
-			Notification notification = mBuilder.build();
-			mNotificationManager.notify(2342, notification);
-		}
-	}
-
-	private static boolean wasHighlightedOrPrivate(Conversation conversation) {
-		List<Message> messages = conversation.getMessages();
-		String nick = conversation.getMucOptions().getActualNick();
-		Pattern highlight = generateNickHighlightPattern(nick);
-		for (int i = messages.size() - 1; i >= 0; --i) {
-			if (messages.get(i).isRead()) {
-				break;
-			} else {
-				Matcher m = highlight.matcher(messages.get(i).getBody());
-				if (m.find()
-						|| messages.get(i).getType() == Message.TYPE_PRIVATE) {
-					return true;
-				}
-			}
-		}
-		return false;
-	}
-
 	public static void prepareContactBadge(final Activity activity,
 			QuickContactBadge badge, final Contact contact, Context context) {
 		if (contact.getSystemAccount() != null) {
@@ -511,6 +336,7 @@ public class UIHelper {
 		badge.setImageBitmap(contact.getImage(72, context));
 	}
 
+	@SuppressLint("InflateParams")
 	public static AlertDialog getVerifyFingerprintDialog(
 			final ConversationActivity activity,
 			final Conversation conversation, final View msg) {

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

@@ -166,8 +166,14 @@ public class XmppConnection implements Runnable {
 							+ ":" + srvRecordPort);
 					socket = new Socket(srvRecordServer, srvRecordPort);
 				}
-			} else {
+			} 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);
@@ -307,7 +313,8 @@ public class XmppConnection implements Runnable {
 				} catch (NumberFormatException e) {
 
 				}
-				changeStatus(Account.STATUS_ONLINE);
+				sendInitialPing();
+
 			} else if (nextTag.isStart("r")) {
 				tagReader.readElement(nextTag);
 				AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
@@ -348,6 +355,22 @@ public class XmppConnection implements Runnable {
 		}
 	}
 
+	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;
@@ -372,8 +395,11 @@ public class XmppConnection implements Runnable {
 		while (!nextTag.isEnd(element.getName())) {
 			if (!nextTag.isNo()) {
 				Element child = tagReader.readElement(nextTag);
-				if ((packetType == PACKET_IQ)
-						&& ("jingle".equals(child.getName()))) {
+				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());
 				}
@@ -410,7 +436,9 @@ public class XmppConnection implements Runnable {
 				}
 
 				packetCallbacks.remove(packet.getId());
-			} else if (this.unregisteredIqListener != null) {
+			} else if ((packet.getType() == IqPacket.TYPE_GET || packet
+					.getType() == IqPacket.TYPE_SET)
+					&& this.unregisteredIqListener != null) {
 				this.unregisteredIqListener.onIqPacketReceived(account, packet);
 			}
 		}
@@ -657,7 +685,7 @@ public class XmppConnection implements Runnable {
 				if (bind != null) {
 					Element jid = bind.findChild("jid");
 					if (jid != null && jid.getContent() != null) {
-						account.setResource(jid.getContent().split("/",2)[1]);
+						account.setResource(jid.getContent().split("/", 2)[1]);
 						if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
 							smVersion = 3;
 							EnablePacket enable = new EnablePacket(smVersion);
@@ -677,7 +705,7 @@ public class XmppConnection implements Runnable {
 						if (bindListener != null) {
 							bindListener.onBind(account);
 						}
-						changeStatus(Account.STATUS_ONLINE);
+						sendInitialPing();
 					} else {
 						disconnect(true);
 					}
@@ -882,8 +910,7 @@ public class XmppConnection implements Runnable {
 	}
 
 	public void disconnect(boolean force) {
-		changeStatus(Account.STATUS_OFFLINE);
-		Log.d(Config.LOGTAG, "disconnecting");
+		Log.d(Config.LOGTAG, account.getJid()+": disconnecting");
 		try {
 			if (force) {
 				socket.close();
@@ -901,6 +928,7 @@ public class XmppConnection implements Runnable {
 								Thread.sleep(100);
 							}
 							tagWriter.writeTag(Tag.end("stream:stream"));
+							socket.close();
 						} catch (IOException e) {
 							Log.d(Config.LOGTAG,
 									"io exception during disconnect");

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

@@ -88,8 +88,8 @@ public class JingleConnection implements Downloadable {
 				sendSuccess();
 				if (acceptedAutomatically) {
 					message.markUnread();
-					JingleConnection.this.mXmppConnectionService.notifyUi(
-							message.getConversation(), true);
+					JingleConnection.this.mXmppConnectionService
+							.getNotificationService().push(message);
 				}
 				BitmapFactory.Options options = new BitmapFactory.Options();
 				options.inJustDecodeBounds = true;
@@ -256,12 +256,12 @@ public class JingleConnection implements Downloadable {
 		this.status = STATUS_INITIATED;
 		Conversation conversation = this.mXmppConnectionService
 				.findOrCreateConversation(account,
-						packet.getFrom().split("/",2)[0], false);
+						packet.getFrom().split("/", 2)[0], false);
 		this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
 		this.message.setType(Message.TYPE_IMAGE);
 		this.message.setStatus(Message.STATUS_RECEIVED_OFFER);
 		this.message.setDownloadable(this);
-		String[] fromParts = packet.getFrom().split("/",2);
+		String[] fromParts = packet.getFrom().split("/", 2);
 		this.message.setPresence(fromParts[1]);
 		this.account = account;
 		this.initiator = packet.getFrom();
@@ -319,8 +319,8 @@ public class JingleConnection implements Downloadable {
 										+ " allowed size:"
 										+ this.mJingleConnectionManager
 												.getAutoAcceptFileSize());
-						this.mXmppConnectionService
-								.notifyUi(conversation, true);
+						this.mXmppConnectionService.getNotificationService()
+								.push(message);
 					}
 					this.file = this.mXmppConnectionService.getFileBackend()
 							.getJingleFile(message, false);