basic last seen feature. no peristancy just yet. no polish

iNPUTmice created

Change summary

res/layout/fragment_conversation.xml                    | 188 +-
res/values/strings.xml                                  |   4 
res/xml/preferences.xml                                 |   5 
src/eu/siacs/conversations/entities/Contact.java        |   8 
src/eu/siacs/conversations/entities/Message.java        |   9 
src/eu/siacs/conversations/parser/MessageParser.java    |  23 
src/eu/siacs/conversations/ui/ConversationActivity.java | 624 ++++++----
src/eu/siacs/conversations/ui/ConversationFragment.java |  62 
src/eu/siacs/conversations/utils/UIHelper.java          |  13 
9 files changed, 556 insertions(+), 380 deletions(-)

Detailed changes

res/layout/fragment_conversation.xml 🔗

@@ -3,32 +3,33 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="#e5e5e5">
+    android:background="#e5e5e5" >
 
     <RelativeLayout
-        android:background="#eee"
         android:id="@+id/textsend"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
-        android:layout_alignParentLeft="true">
+        android:layout_alignParentLeft="true"
+        android:background="#eee" >
 
         <EditText
             android:id="@+id/textinput"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:minHeight="48dp"
             android:layout_alignParentLeft="true"
-            android:paddingBottom="12dp"
-            android:paddingLeft="8dp"
-            android:paddingRight="8dp"
-            android:paddingTop="12dp"
             android:layout_toLeftOf="@+id/textSendButton"
             android:background="#eee"
             android:ems="10"
             android:inputType="textShortMessage|textMultiLine|textCapSentences"
-            android:minLines="1" >
-                    <requestFocus />
+            android:minHeight="48dp"
+            android:minLines="1"
+            android:paddingBottom="12dp"
+            android:paddingLeft="8dp"
+            android:paddingRight="8dp"
+            android:paddingTop="12dp" >
+
+            <requestFocus />
         </EditText>
 
         <ImageButton
@@ -52,101 +53,118 @@
         android:divider="@null"
         android:dividerHeight="0dp"
         android:listSelector="@android:color/transparent"
+        android:stackFromBottom="true"
         android:transcriptMode="alwaysScroll"
-        tools:listitem="@layout/message_sent"
-        android:stackFromBottom="true">
-
+        tools:listitem="@layout/message_sent" >
     </ListView>
+
     <LinearLayout
         android:id="@+id/info_box"
-         android:layout_height="wrap_content"
-         android:layout_width="fill_parent"
-         android:orientation="vertical"
-        >
-        
-        <LinearLayout
-        android:id="@+id/muc_error"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
-        android:background="@drawable/redbackground"
-        android:orientation="vertical"
-        android:visibility="gone"
-        >
+        android:orientation="vertical" >
 
-        <TextView
-            android:id="@+id/muc_error_msg"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textColor="#eee"
-            android:textStyle="bold"
-            android:padding="8dp"
-            android:textSize="20sp"/>
-        <TextView 
-            android:layout_width="wrap_content"
+        <LinearLayout
+            android:id="@+id/muc_error"
+            android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:text="Click to edit conference details"
-            android:textColor="#eee"
-            android:paddingLeft="8dp"
-            android:paddingBottom="8dp"
-            android:textSize="14sp"/>
-        
-    </LinearLayout>
-        
-        
-    <LinearLayout
-        android:id="@+id/new_fingerprint"
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:background="@drawable/redbackground"
-        android:orientation="vertical"
-        android:visibility="gone"
-        >
+            android:background="@drawable/redbackground"
+            android:orientation="vertical"
+            android:visibility="gone" >
 
-        <TextView
-            android:layout_width="wrap_content"
+            <TextView
+                android:id="@+id/muc_error_msg"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:padding="8dp"
+                android:textColor="#eee"
+                android:textSize="20sp"
+                android:textStyle="bold" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingBottom="8dp"
+                android:paddingLeft="8dp"
+                android:text="Click to edit conference details"
+                android:textColor="#eee"
+                android:textSize="14sp" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/new_fingerprint"
+            android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:text="Unknown OTR Fingerprint"
-            android:textColor="#eee"
-            android:textStyle="bold"
-            android:padding="8dp"
-            android:textSize="20sp"/>
-        <TextView 
-            android:layout_width="wrap_content"
+            android:background="@drawable/redbackground"
+            android:orientation="vertical"
+            android:visibility="gone" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:padding="8dp"
+                android:text="Unknown OTR Fingerprint"
+                android:textColor="#eee"
+                android:textSize="20sp"
+                android:textStyle="bold" />
+
+            <TextView
+                android:id="@+id/otr_fingerprint"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingBottom="8dp"
+                android:paddingLeft="8dp"
+                android:textColor="#eee"
+                android:textSize="14sp"
+                android:typeface="monospace" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/pgp_keyentry"
+            android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:id="@+id/otr_fingerprint"
-            android:textColor="#eee"
-            android:paddingLeft="8dp"
-            android:paddingBottom="8dp"
-            android:textSize="14sp"
-            android:typeface="monospace"/>
-        
+            android:background="@drawable/bluebackground"
+            android:orientation="vertical"
+            android:visibility="gone" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:padding="8dp"
+                android:text="OpenPGP encrypted messages found"
+                android:textColor="#eee"
+                android:textSize="20sp"
+                android:textStyle="bold" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingBottom="8dp"
+                android:paddingLeft="8dp"
+                android:text="Click here to enter passphrase and decrypt messages"
+                android:textColor="#eee"
+                android:textSize="14sp" />
+        </LinearLayout>
     </LinearLayout>
+
     <LinearLayout
-        android:id="@+id/pgp_keyentry"
+        android:id="@+id/last_seen"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
-        android:background="@drawable/bluebackground"
+        android:background="#7f333333"
         android:orientation="vertical"
         android:visibility="gone"
-        >
+        android:layout_below="@+id/info_box">
 
         <TextView
+            android:id="@+id/last_seen_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="OpenPGP encrypted messages found"
-            android:textColor="#eee"
-            android:textStyle="bold"
-            android:padding="8dp"
-            android:textSize="20sp"/>
-        <TextView 
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textColor="#eee"
-            android:text="Click here to enter passphrase and decrypt messages"
-            android:paddingLeft="8dp"
-            android:paddingBottom="8dp"
-            android:textSize="14sp"/>
-        
-    </LinearLayout>
+            android:layout_gravity="center"
+            android:padding="4dp"
+            android:text="@string/last_seen"
+            android:textColor="#e5e5e5"
+            android:textSize="14sp" />
     </LinearLayout>
+
 </RelativeLayout>

res/values/strings.xml 🔗

@@ -147,6 +147,8 @@
     <string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string>
     <string name="pref_confirm_messages">Confirm Messages</string>
     <string name="pref_confirm_messages_summary">Let your contact know when you have received and read a message</string>
+    <string name="pref_show_last_seen">Display last seen</string>
+    <string name="pref_show_last_seen_summary">Display the latest time a contact has been seen online</string>
     <string name="openpgp_error">OpenKeychain reporeted an error</string>
     <string name="error_decrypting_file">I/O Error decrypting file</string>
     <string name="error_copying_image_file">Error copying image file.</string>
@@ -234,4 +236,6 @@
     <string name="hours">hours</string>
     <string name="mins">mins</string>
     <string name="missing_public_keys">Missing public key announcements</string>
+    <string name="last_seen">last seen %1$s ago on %2$s</string>
+    <string name="never_seen">never seen</string>
 </resources>

res/xml/preferences.xml 🔗

@@ -72,6 +72,11 @@
             android:title="@string/pref_conference_name"
             android:summary="@string/pref_conference_name_summary"
             android:defaultValue="true"/>
+        <CheckBoxPreference 
+            android:key="show_last_seen"
+            android:title="@string/pref_show_last_seen"
+            android:summary="@string/pref_show_last_seen_summary"
+            android:defaultValue="false"/>
     </PreferenceCategory>
     <PreferenceCategory
         android:title="@string/pref_advanced_options">

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

@@ -11,7 +11,6 @@ import org.json.JSONObject;
 import eu.siacs.conversations.xml.Element;
 import android.content.ContentValues;
 import android.database.Cursor;
-import android.util.Log;
 
 public class Contact {
 	public static final String TABLENAME = "contacts";
@@ -38,6 +37,8 @@ public class Contact {
 	protected Account account;
 
 	protected boolean inRoster = true;
+	
+	public Lastseen lastseen = new Lastseen();
 
 	public Contact(String account, String systemName, String serverName,
 			String jid, int subscription, String photoUri,
@@ -305,4 +306,9 @@ public class Contact {
 		public static final int DIRTY_PUSH = 6;
 		public static final int DIRTY_DELETE = 7;
 	}
+	
+	public class Lastseen {
+		public long time = 0;
+		public String presence = null;
+	}
 }

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

@@ -203,6 +203,15 @@ public class Message extends AbstractEntity {
 		this.counterpart = this.counterpart.split("/")[0] + "/" + presence;
 	}
 	
+	public String getPresence() {
+		String[] counterparts = this.counterpart.split("/");
+		if (counterparts.length == 2) {
+			return counterparts[1];
+		} else {
+			return null;
+		}
+	}
+	
 	public void setJingleConnection(JingleConnection connection) {
 		this.jingleConnection = connection;
 	}

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

@@ -6,6 +6,7 @@ import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
 import android.util.Log;
 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.XmppConnectionService;
@@ -26,6 +27,7 @@ public class MessageParser {
 		Conversation conversation = mXmppConnectionService
 				.findOrCreateConversation(account, fromParts[0], false);
 		conversation.setLatestMarkableMessageId(getMarkableMessageId(packet));
+		updateLastseen(packet, account);
 		String pgpBody = getPgpBody(packet);
 		if (pgpBody != null) {
 			return new Message(conversation, packet.getFrom(), pgpBody,
@@ -43,6 +45,7 @@ public class MessageParser {
 		String[] fromParts = packet.getFrom().split("/");
 		Conversation conversation = mXmppConnectionService
 				.findOrCreateConversation(account, fromParts[0], false);
+		updateLastseen(packet, account);
 		String body = packet.getBody();
 		if (!conversation.hasValidOtrSession()) {
 			if (properlyAddressed) {
@@ -171,6 +174,7 @@ public class MessageParser {
 			return null; // either malformed or boring
 		if (status == Message.STATUS_RECIEVED) {
 			fullJid = message.getAttribute("from");
+			updateLastseen(message, account);
 		} else {
 			fullJid = message.getAttribute("to");
 		}
@@ -211,4 +215,23 @@ public class MessageParser {
 			return null;
 		}
 	}
+	
+	private void updateLastseen(Element message, Account account) {
+		String[] fromParts = message.getAttribute("from").split("/");
+		String from = fromParts[0];
+		String presence = null;
+		if (fromParts.length >= 2) {
+			presence = fromParts[1];
+		}
+		Contact contact = account.getRoster().getContact(from);
+		if (presence!=null) {
+			contact.lastseen.presence = presence;
+			contact.lastseen.time = System.currentTimeMillis();
+		} else if ((contact.getPresences().size() == 1)&&(contact.getPresences().containsKey(contact.lastseen.presence))) {
+			contact.lastseen.time = System.currentTimeMillis();
+		} else {
+			contact.lastseen.presence = null;
+			contact.lastseen.time = System.currentTimeMillis();
+		}
+	}
 }

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

@@ -69,7 +69,7 @@ public class ConversationActivity extends XmppActivity {
 	private static final int REQUEST_RECORD_AUDIO = 0x46189;
 	private static final int REQUEST_SEND_PGP_IMAGE = 0x53883;
 	public static final int REQUEST_ENCRYPT_MESSAGE = 0x378018;
-	
+
 	private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x92734;
 	private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x84123;
 	private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x75291;
@@ -79,11 +79,12 @@ public class ConversationActivity extends XmppActivity {
 	private List<Conversation> conversationList = new ArrayList<Conversation>();
 	private Conversation selectedConversation = null;
 	private ListView listView;
-	
+
 	private boolean paneShouldBeOpen = true;
 	private boolean useSubject = true;
+	private boolean showLastseen = false;
 	private ArrayAdapter<Conversation> listAdapter;
-	
+
 	public Message pendingMessage = null;
 
 	private OnConversationListChangedListener onConvChanged = new OnConversationListChangedListener() {
@@ -113,7 +114,7 @@ public class ConversationActivity extends XmppActivity {
 			});
 		}
 	};
-	
+
 	protected ConversationActivity activity = this;
 	private DisplayMetrics metrics;
 	private Toast prepareImageToast;
@@ -125,7 +126,7 @@ public class ConversationActivity extends XmppActivity {
 	public Conversation getSelectedConversation() {
 		return this.selectedConversation;
 	}
-	
+
 	public void setSelectedConversation(Conversation conversation) {
 		this.selectedConversation = conversation;
 	}
@@ -146,7 +147,7 @@ public class ConversationActivity extends XmppActivity {
 	protected void onCreate(Bundle savedInstanceState) {
 
 		metrics = getResources().getDisplayMetrics();
-		
+
 		super.onCreate(savedInstanceState);
 
 		setContentView(R.layout.fragment_conversations_overview);
@@ -182,15 +183,18 @@ public class ConversationActivity extends XmppActivity {
 				convName.setText(conv.getName(useSubject));
 				TextView convLastMsg = (TextView) view
 						.findViewById(R.id.conversation_lastmsg);
-				ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage);
-				
+				ImageView imagePreview = (ImageView) view
+						.findViewById(R.id.conversation_lastimage);
+
 				Message latestMessage = conv.getLatestMessage();
-				
+
 				if (latestMessage.getType() == Message.TYPE_TEXT) {
-					if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP)&&(latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
+					if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP)
+							&& (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
 						convLastMsg.setText(conv.getLatestMessage().getBody());
 					} else {
-						convLastMsg.setText(getText(R.string.encrypted_message_received));
+						convLastMsg
+								.setText(getText(R.string.encrypted_message_received));
 					}
 					convLastMsg.setVisibility(View.VISIBLE);
 					imagePreview.setVisibility(View.GONE);
@@ -203,16 +207,16 @@ public class ConversationActivity extends XmppActivity {
 						convLastMsg.setVisibility(View.VISIBLE);
 						imagePreview.setVisibility(View.GONE);
 						if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) {
-							convLastMsg.setText(getText(R.string.image_offered_for_download));
+							convLastMsg
+									.setText(getText(R.string.image_offered_for_download));
 						} else if (latestMessage.getStatus() == Message.STATUS_RECIEVING) {
-							convLastMsg.setText(getText(R.string.receiving_image));
+							convLastMsg
+									.setText(getText(R.string.receiving_image));
 						} else {
 							convLastMsg.setText("");
 						}
 					}
 				}
-				
-				
 
 				if (!conv.isRead()) {
 					convName.setTypeface(null, Typeface.BOLD);
@@ -223,14 +227,14 @@ public class ConversationActivity extends XmppActivity {
 				}
 
 				((TextView) view.findViewById(R.id.conversation_lastupdate))
-						.setText(UIHelper.readableTimeDifference(getContext(), conv
-								.getLatestMessage().getTimeSent()));
+						.setText(UIHelper.readableTimeDifference(getContext(),
+								conv.getLatestMessage().getTimeSent()));
 
 				ImageView profilePicture = (ImageView) view
 						.findViewById(R.id.conversation_image);
-				profilePicture.setImageBitmap(UIHelper.getContactPicture(
-						conv, 56, activity.getApplicationContext(), false));
-				
+				profilePicture.setImageBitmap(UIHelper.getContactPicture(conv,
+						56, activity.getApplicationContext(), false));
+
 				return view;
 			}
 
@@ -266,6 +270,11 @@ public class ConversationActivity extends XmppActivity {
 				getActionBar().setTitle(R.string.app_name);
 				invalidateOptionsMenu();
 				hideKeyboard();
+				ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+						.findFragmentByTag("conversation");
+				if (selectedFragment != null) {
+					selectedFragment.lastSeen.setVisibility(View.GONE);
+				}
 			}
 
 			@Override
@@ -279,11 +288,17 @@ public class ConversationActivity extends XmppActivity {
 							getSelectedConversation().getName(useSubject));
 					invalidateOptionsMenu();
 					if (!getSelectedConversation().isRead()) {
-						xmppConnectionService.markRead(getSelectedConversation());
+						xmppConnectionService
+								.markRead(getSelectedConversation());
 						UIHelper.updateNotification(getApplicationContext(),
 								getConversationList(), null, false);
 						listView.invalidateViews();
 					}
+					ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
+							.findFragmentByTag("conversation");
+					if ((selectedFragment != null) && (showLastseen())) {
+						selectedFragment.lastSeen.setVisibility(View.VISIBLE);
+					}
 				}
 			}
 
@@ -307,7 +322,8 @@ public class ConversationActivity extends XmppActivity {
 		MenuItem menuInviteContacts = (MenuItem) menu
 				.findItem(R.id.action_invite);
 		MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file);
-		MenuItem menuClearHistory = (MenuItem) menu.findItem(R.id.action_clear_history);
+		MenuItem menuClearHistory = (MenuItem) menu
+				.findItem(R.id.action_clear_history);
 
 		if ((spl.isOpen() && (spl.isSlideable()))) {
 			menuArchive.setVisible(false);
@@ -336,27 +352,34 @@ public class ConversationActivity extends XmppActivity {
 		}
 		return true;
 	}
-	
+
 	private void selectPresenceToAttachFile(final int attachmentChoice) {
 		selectPresence(getSelectedConversation(), new OnPresenceSelected() {
-			
+
 			@Override
 			public void onPresenceSelected(boolean success, String presence) {
 				if (success) {
-					if (attachmentChoice==ATTACHMENT_CHOICE_TAKE_PHOTO) {
-						Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
-						takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, ImageProvider.getIncomingContentUri());
-						if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
-							startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
+					if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) {
+						Intent takePictureIntent = new Intent(
+								MediaStore.ACTION_IMAGE_CAPTURE);
+						takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
+								ImageProvider.getIncomingContentUri());
+						if (takePictureIntent
+								.resolveActivity(getPackageManager()) != null) {
+							startActivityForResult(takePictureIntent,
+									REQUEST_IMAGE_CAPTURE);
 						}
-					} else if (attachmentChoice==ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
+					} else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
 						Intent attachFileIntent = new Intent();
 						attachFileIntent.setType("image/*");
 						attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
-						Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file));
-						startActivityForResult(chooser,	REQUEST_ATTACH_FILE_DIALOG);
-					} else if (attachmentChoice==ATTACHMENT_CHOICE_RECORD_VOICE) {
-						Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+						Intent chooser = Intent.createChooser(attachFileIntent,
+								getString(R.string.attach_file));
+						startActivityForResult(chooser,
+								REQUEST_ATTACH_FILE_DIALOG);
+					} else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
+						Intent intent = new Intent(
+								MediaStore.Audio.Media.RECORD_SOUND_ACTION);
 						startActivityForResult(intent, REQUEST_RECORD_AUDIO);
 					}
 				}
@@ -365,45 +388,50 @@ public class ConversationActivity extends XmppActivity {
 			@Override
 			public void onSendPlainTextInstead() {
 				// TODO Auto-generated method stub
-				
+
 			}
-		},"file");
+		}, "file");
 	}
 
 	private void attachFile(final int attachmentChoice) {
 		final Conversation conversation = getSelectedConversation();
 		if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 			if (hasPgp()) {
-				if (conversation.getContact().getPgpKeyId()!=0) {
-					xmppConnectionService.getPgpEngine().hasKey(conversation.getContact(), new UiCallback() {
-						
-						@Override
-						public void userInputRequried(PendingIntent pi) {
-							ConversationActivity.this.runIntent(pi, attachmentChoice);
-						}
-						
-						@Override
-						public void success() {
-							selectPresenceToAttachFile(attachmentChoice);
-						}
-						
-						@Override
-						public void error(int error) {
-							displayErrorDialog(error);
-						}
-					});
+				if (conversation.getContact().getPgpKeyId() != 0) {
+					xmppConnectionService.getPgpEngine().hasKey(
+							conversation.getContact(), new UiCallback() {
+
+								@Override
+								public void userInputRequried(PendingIntent pi) {
+									ConversationActivity.this.runIntent(pi,
+											attachmentChoice);
+								}
+
+								@Override
+								public void success() {
+									selectPresenceToAttachFile(attachmentChoice);
+								}
+
+								@Override
+								public void error(int error) {
+									displayErrorDialog(error);
+								}
+							});
 				} else {
 					final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
 							.findFragmentByTag("conversation");
 					if (fragment != null) {
-						fragment.showNoPGPKeyDialog(false,new OnClickListener() {
-							
-							@Override
-							public void onClick(DialogInterface dialog, int which) {
-								conversation.setNextEncryption(Message.ENCRYPTION_NONE);
-								selectPresenceToAttachFile(attachmentChoice);
-							}
-						});
+						fragment.showNoPGPKeyDialog(false,
+								new OnClickListener() {
+
+									@Override
+									public void onClick(DialogInterface dialog,
+											int which) {
+										conversation
+												.setNextEncryption(Message.ENCRYPTION_NONE);
+										selectPresenceToAttachFile(attachmentChoice);
+									}
+								});
 					}
 				}
 			}
@@ -414,29 +442,36 @@ public class ConversationActivity extends XmppActivity {
 			builder.setTitle(getString(R.string.otr_file_transfer));
 			builder.setMessage(getString(R.string.otr_file_transfer_msg));
 			builder.setNegativeButton(getString(R.string.cancel), null);
-			if (conversation.getContact().getPgpKeyId()==0) {
-				builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() {
-					
-					@Override
-					public void onClick(DialogInterface dialog, int which) {
-						conversation.setNextEncryption(Message.ENCRYPTION_NONE);
-						attachFile(attachmentChoice);
-					}
-				});
+			if (conversation.getContact().getPgpKeyId() == 0) {
+				builder.setPositiveButton(getString(R.string.send_unencrypted),
+						new OnClickListener() {
+
+							@Override
+							public void onClick(DialogInterface dialog,
+									int which) {
+								conversation
+										.setNextEncryption(Message.ENCRYPTION_NONE);
+								attachFile(attachmentChoice);
+							}
+						});
 			} else {
-				builder.setPositiveButton(getString(R.string.use_pgp_encryption), new OnClickListener() {
-					
-					@Override
-					public void onClick(DialogInterface dialog, int which) {
-						conversation.setNextEncryption(Message.ENCRYPTION_PGP);
-						attachFile(attachmentChoice);
-					}
-				});
+				builder.setPositiveButton(
+						getString(R.string.use_pgp_encryption),
+						new OnClickListener() {
+
+							@Override
+							public void onClick(DialogInterface dialog,
+									int which) {
+								conversation
+										.setNextEncryption(Message.ENCRYPTION_PGP);
+								attachFile(attachmentChoice);
+							}
+						});
 			}
 			builder.create().show();
 		}
 	}
-	
+
 	@Override
 	public boolean onOptionsItemSelected(MenuItem item) {
 		switch (item.getItemId()) {
@@ -447,24 +482,25 @@ public class ConversationActivity extends XmppActivity {
 			View menuAttachFile = findViewById(R.id.action_attach_file);
 			PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile);
 			attachFilePopup.inflate(R.menu.attachment_choices);
-			attachFilePopup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
-				
-				@Override
-				public boolean onMenuItemClick(MenuItem item) {
-					switch (item.getItemId()) {
-					case R.id.attach_choose_picture:
-						attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
-						break;
-					case R.id.attach_take_picture:
-						attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
-						break;
-					case R.id.attach_record_voice:
-						attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
-						break;
-					}
-					return false;
-				}
-			});
+			attachFilePopup
+					.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+						@Override
+						public boolean onMenuItemClick(MenuItem item) {
+							switch (item.getItemId()) {
+							case R.id.attach_choose_picture:
+								attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+								break;
+							case R.id.attach_take_picture:
+								attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+								break;
+							case R.id.attach_record_voice:
+								attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
+								break;
+							}
+							return false;
+						}
+					});
 			attachFilePopup.show();
 			break;
 		case R.id.action_add:
@@ -478,8 +514,9 @@ public class ConversationActivity extends XmppActivity {
 			if (contact.showInRoster()) {
 				Intent intent = new Intent(this, ContactDetailsActivity.class);
 				intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
-				intent.putExtra("account", this.getSelectedConversation().getAccount().getJid());
-				intent.putExtra("contact",contact.getJid());
+				intent.putExtra("account", this.getSelectedConversation()
+						.getAccount().getJid());
+				intent.putExtra("contact", contact.getJid());
 				startActivity(intent);
 			} else {
 				showAddToRosterDialog(getSelectedConversation());
@@ -511,25 +548,31 @@ public class ConversationActivity extends XmppActivity {
 					public boolean onMenuItemClick(MenuItem item) {
 						switch (item.getItemId()) {
 						case R.id.encryption_choice_none:
-							conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+							conversation
+									.setNextEncryption(Message.ENCRYPTION_NONE);
 							item.setChecked(true);
 							break;
 						case R.id.encryption_choice_otr:
-							conversation.setNextEncryption(Message.ENCRYPTION_OTR);
+							conversation
+									.setNextEncryption(Message.ENCRYPTION_OTR);
 							item.setChecked(true);
 							break;
 						case R.id.encryption_choice_pgp:
 							if (hasPgp()) {
-								if (conversation.getAccount().getKeys().has("pgp_signature")) {
-									conversation.setNextEncryption(Message.ENCRYPTION_PGP);
+								if (conversation.getAccount().getKeys()
+										.has("pgp_signature")) {
+									conversation
+											.setNextEncryption(Message.ENCRYPTION_PGP);
 									item.setChecked(true);
 								} else {
-									announcePgp(conversation.getAccount(),conversation);
+									announcePgp(conversation.getAccount(),
+											conversation);
 								}
 							}
 							break;
 						default:
-							conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+							conversation
+									.setNextEncryption(Message.ENCRYPTION_NONE);
 							break;
 						}
 						fragment.updateChatMsgHint();
@@ -537,7 +580,8 @@ public class ConversationActivity extends XmppActivity {
 					}
 				});
 				popup.inflate(R.menu.encryption_choices);
-				MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
+				MenuItem otr = popup.getMenu().findItem(
+						R.id.encryption_choice_otr);
 				if (conversation.getMode() == Conversation.MODE_MULTI) {
 					otr.setEnabled(false);
 				}
@@ -570,7 +614,7 @@ public class ConversationActivity extends XmppActivity {
 		}
 		return super.onOptionsItemSelected(item);
 	}
-	
+
 	private void endConversation(Conversation conversation) {
 		conversation.setStatus(Conversation.STATUS_ARCHIVED);
 		paneShouldBeOpen = true;
@@ -586,20 +630,24 @@ public class ConversationActivity extends XmppActivity {
 	protected void clearHistoryDialog(final Conversation conversation) {
 		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 		builder.setTitle(getString(R.string.clear_conversation_history));
-		View dialogView = getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
-		final CheckBox endConversationCheckBox = (CheckBox) dialogView.findViewById(R.id.end_conversation_checkbox);
+		View dialogView = getLayoutInflater().inflate(
+				R.layout.dialog_clear_history, null);
+		final CheckBox endConversationCheckBox = (CheckBox) dialogView
+				.findViewById(R.id.end_conversation_checkbox);
 		builder.setView(dialogView);
 		builder.setNegativeButton(getString(R.string.cancel), null);
-		builder.setPositiveButton(getString(R.string.delete_messages), new OnClickListener() {
-			
-			@Override
-			public void onClick(DialogInterface dialog, int which) {
-				activity.xmppConnectionService.clearConversationHistory(conversation);
-				if (endConversationCheckBox.isChecked()) {
-					endConversation(conversation);
-				}
-			}
-		});
+		builder.setPositiveButton(getString(R.string.delete_messages),
+				new OnClickListener() {
+
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						activity.xmppConnectionService
+								.clearConversationHistory(conversation);
+						if (endConversationCheckBox.isChecked()) {
+							endConversation(conversation);
+						}
+					}
+				});
 		builder.create().show();
 	}
 
@@ -626,10 +674,10 @@ public class ConversationActivity extends XmppActivity {
 	}
 
 	@Override
-	protected void onNewIntent (Intent intent) {
-		if ((Intent.ACTION_VIEW.equals(intent.getAction())&&(VIEW_CONVERSATION.equals(intent.getType())))) {
-			String convToView = (String) intent.getExtras().get(
-					CONVERSATION);
+	protected void onNewIntent(Intent intent) {
+		if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION
+				.equals(intent.getType())))) {
+			String convToView = (String) intent.getExtras().get(CONVERSATION);
 			updateConversationList();
 			for (int i = 0; i < conversationList.size(); ++i) {
 				if (conversationList.get(i).getUuid().equals(convToView)) {
@@ -642,13 +690,14 @@ public class ConversationActivity extends XmppActivity {
 			swapConversationFragment().setText(text);
 		}
 	}
-	
+
 	@Override
 	public void onStart() {
 		super.onStart();
 		SharedPreferences preferences = PreferenceManager
 				.getDefaultSharedPreferences(this);
 		this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
+		this.showLastseen = preferences.getBoolean("show_last_seen", false);
 		if (this.xmppConnectionServiceBound) {
 			this.onBackendConnected();
 		}
@@ -722,7 +771,8 @@ public class ConversationActivity extends XmppActivity {
 	}
 
 	@Override
-	protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
+	protected void onActivityResult(int requestCode, int resultCode,
+			final Intent data) {
 		super.onActivityResult(requestCode, resultCode, data);
 		if (resultCode == RESULT_OK) {
 			if (requestCode == REQUEST_DECRYPT_PGP) {
@@ -732,62 +782,68 @@ public class ConversationActivity extends XmppActivity {
 					selectedFragment.hidePgpPassphraseBox();
 				}
 			} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
-				attachImageToConversation(getSelectedConversation(),data.getData());
+				attachImageToConversation(getSelectedConversation(),
+						data.getData());
 			} else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
-				
+
 			} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
 				attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
 			} else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
 				attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
 			} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
-				announcePgp(getSelectedConversation().getAccount(),getSelectedConversation());
+				announcePgp(getSelectedConversation().getAccount(),
+						getSelectedConversation());
 			} else if (requestCode == REQUEST_ENCRYPT_MESSAGE) {
 				encryptTextMessage();
 			} else if (requestCode == REQUEST_IMAGE_CAPTURE) {
 				attachImageToConversation(getSelectedConversation(), null);
 			} else if (requestCode == REQUEST_RECORD_AUDIO) {
-				Log.d("xmppService",data.getData().toString());
-				attachAudioToConversation(getSelectedConversation(),data.getData());
+				Log.d("xmppService", data.getData().toString());
+				attachAudioToConversation(getSelectedConversation(),
+						data.getData());
 			} else {
-				Log.d(LOGTAG,"unknown result code:"+requestCode);
+				Log.d(LOGTAG, "unknown result code:" + requestCode);
 			}
 		}
 	}
-	
+
 	private void attachAudioToConversation(Conversation conversation, Uri uri) {
-		
+
 	}
 
 	private void attachImageToConversation(Conversation conversation, Uri uri) {
-		prepareImageToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
+		prepareImageToast = Toast.makeText(getApplicationContext(),
+				getText(R.string.preparing_image), Toast.LENGTH_LONG);
 		prepareImageToast.show();
-		pendingMessage = xmppConnectionService.attachImageToConversation(conversation, uri, new UiCallback() {
-				
-			@Override
-			public void userInputRequried(PendingIntent pi) {
-				hidePrepareImageToast();
-				ConversationActivity.this.runIntent(pi, ConversationActivity.REQUEST_SEND_PGP_IMAGE);
-			}
-			
-			@Override
-			public void success() {
-				sendPendingImageMessage();
-				hidePrepareImageToast();
-			}
-			
-			@Override
-			public void error(int error) {
-				hidePrepareImageToast();
-				pendingMessage = null;
-				displayErrorDialog(error);
-			}
-		});
+		pendingMessage = xmppConnectionService.attachImageToConversation(
+				conversation, uri, new UiCallback() {
+
+					@Override
+					public void userInputRequried(PendingIntent pi) {
+						hidePrepareImageToast();
+						ConversationActivity.this.runIntent(pi,
+								ConversationActivity.REQUEST_SEND_PGP_IMAGE);
+					}
+
+					@Override
+					public void success() {
+						sendPendingImageMessage();
+						hidePrepareImageToast();
+					}
+
+					@Override
+					public void error(int error) {
+						hidePrepareImageToast();
+						pendingMessage = null;
+						displayErrorDialog(error);
+					}
+				});
 	}
-	
+
 	private void hidePrepareImageToast() {
-		if (prepareImageToast!=null) {
+		if (prepareImageToast != null) {
 			runOnUiThread(new Runnable() {
-				
+
 				@Override
 				public void run() {
 					prepareImageToast.cancel();
@@ -803,41 +859,46 @@ public class ConversationActivity extends XmppActivity {
 		xmppConnectionService.updateUi(pendingMessage.getConversation(), false);
 		pendingMessage = null;
 	}
-	
+
 	public void updateConversationList() {
 		conversationList.clear();
 		conversationList.addAll(xmppConnectionService.getConversations());
 		listView.invalidateViews();
 	}
-	
-	public void selectPresence(final Conversation conversation, final OnPresenceSelected listener, String reason) {
+
+	public void selectPresence(final Conversation conversation,
+			final OnPresenceSelected listener, String reason) {
 		Account account = conversation.getAccount();
 		if (account.getStatus() != Account.STATUS_ONLINE) {
 			AlertDialog.Builder builder = new AlertDialog.Builder(this);
 			builder.setTitle(getString(R.string.not_connected));
 			builder.setIconAttribute(android.R.attr.alertDialogIcon);
 			if ("otr".equals(reason)) {
-				builder.setMessage(getString(R.string.you_are_offline,getString(R.string.otr_messages)));
+				builder.setMessage(getString(R.string.you_are_offline,
+						getString(R.string.otr_messages)));
 			} else if ("file".equals(reason)) {
-				builder.setMessage(getString(R.string.you_are_offline,getString(R.string.files)));
+				builder.setMessage(getString(R.string.you_are_offline,
+						getString(R.string.files)));
 			} else {
 				builder.setMessage(getString(R.string.you_are_offline_blank));
 			}
 			builder.setNegativeButton(getString(R.string.cancel), null);
-			builder.setPositiveButton(getString(R.string.manage_account), new OnClickListener() {
-				
-				@Override
-				public void onClick(DialogInterface dialog, int which) {
-					startActivity(new Intent(activity, ManageAccountActivity.class));
-				}
-			});
+			builder.setPositiveButton(getString(R.string.manage_account),
+					new OnClickListener() {
+
+						@Override
+						public void onClick(DialogInterface dialog, int which) {
+							startActivity(new Intent(activity,
+									ManageAccountActivity.class));
+						}
+					});
 			builder.create().show();
 			listener.onPresenceSelected(false, null);
 		} else {
 			Contact contact = conversation.getContact();
-			if (contact==null) {
+			if (contact == null) {
 				showAddToRosterDialog(conversation);
-				listener.onPresenceSelected(false,null);
+				listener.onPresenceSelected(false, null);
 			} else {
 				Hashtable<String, Integer> presences = contact.getPresences();
 				if (presences.size() == 0) {
@@ -845,13 +906,16 @@ public class ConversationActivity extends XmppActivity {
 					builder.setTitle(getString(R.string.contact_offline));
 					if ("otr".equals(reason)) {
 						builder.setMessage(getString(R.string.contact_offline_otr));
-						builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() {
-							
-							@Override
-							public void onClick(DialogInterface dialog, int which) {
-								listener.onSendPlainTextInstead();
-							}
-						});
+						builder.setPositiveButton(
+								getString(R.string.send_unencrypted),
+								new OnClickListener() {
+
+									@Override
+									public void onClick(DialogInterface dialog,
+											int which) {
+										listener.onSendPlainTextInstead();
+									}
+								});
 					} else if ("file".equals(reason)) {
 						builder.setMessage(getString(R.string.contact_offline_file));
 					}
@@ -870,13 +934,13 @@ public class ConversationActivity extends XmppActivity {
 					presences.keySet().toArray(presencesArray);
 					builder.setItems(presencesArray,
 							new DialogInterface.OnClickListener() {
-	
+
 								@Override
 								public void onClick(DialogInterface dialog,
 										int which) {
 									String presence = presencesArray[which];
 									conversation.setNextPresence(presence);
-									listener.onPresenceSelected(true,presence);
+									listener.onPresenceSelected(true, presence);
 								}
 							});
 					builder.create().show();
@@ -884,137 +948,149 @@ public class ConversationActivity extends XmppActivity {
 			}
 		}
 	}
-	
+
+	public boolean showLastseen() {
+		if (getSelectedConversation() == null) {
+			return false;
+		} else {
+			return this.showLastseen
+					&& getSelectedConversation().getMode() == Conversation.MODE_SINGLE;
+		}
+	}
+
 	private void showAddToRosterDialog(final Conversation conversation) {
 		String jid = conversation.getContactJid();
 		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 		builder.setTitle(jid);
 		builder.setMessage(getString(R.string.not_in_roster));
 		builder.setNegativeButton(getString(R.string.cancel), null);
-		builder.setPositiveButton(getString(R.string.add_contact), new DialogInterface.OnClickListener() {
+		builder.setPositiveButton(getString(R.string.add_contact),
+				new DialogInterface.OnClickListener() {
 
-			@Override
-			public void onClick(DialogInterface dialog, int which) {
-				String jid = conversation.getContactJid();
-				Account account = getSelectedConversation().getAccount();
-				Contact contact = account.getRoster().getContact(jid);
-				xmppConnectionService.createContact(contact);
-			}
-		});
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						String jid = conversation.getContactJid();
+						Account account = getSelectedConversation()
+								.getAccount();
+						Contact contact = account.getRoster().getContact(jid);
+						xmppConnectionService.createContact(contact);
+					}
+				});
 		builder.create().show();
 	}
-	
+
 	public void runIntent(PendingIntent pi, int requestCode) {
 		try {
-			this.startIntentSenderForResult(pi.getIntentSender(),requestCode, null, 0,
-					0, 0);
+			this.startIntentSenderForResult(pi.getIntentSender(), requestCode,
+					null, 0, 0, 0);
 		} catch (SendIntentException e1) {
-			Log.d("xmppService","failed to start intent to send message");
+			Log.d("xmppService", "failed to start intent to send message");
 		}
 	}
-	
-	
+
 	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
-	    private final WeakReference<ImageView> imageViewReference;
-	    private Message message = null;
-
-	    public BitmapWorkerTask(ImageView imageView) {
-	        imageViewReference = new WeakReference<ImageView>(imageView);
-	    }
-
-	    @Override
-	    protected Bitmap doInBackground(Message... params) {
-	        message = params[0];
-	        try {
-				return xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288),false);
+		private final WeakReference<ImageView> imageViewReference;
+		private Message message = null;
+
+		public BitmapWorkerTask(ImageView imageView) {
+			imageViewReference = new WeakReference<ImageView>(imageView);
+		}
+
+		@Override
+		protected Bitmap doInBackground(Message... params) {
+			message = params[0];
+			try {
+				return xmppConnectionService.getFileBackend().getThumbnail(
+						message, (int) (metrics.density * 288), false);
 			} catch (FileNotFoundException e) {
-				Log.d("xmppService","file not found!");
+				Log.d("xmppService", "file not found!");
 				return null;
 			}
-	    }
-
-	    @Override
-	    protected void onPostExecute(Bitmap bitmap) {
-	        if (imageViewReference != null && bitmap != null) {
-	            final ImageView imageView = imageViewReference.get();
-	            if (imageView != null) {
-	                imageView.setImageBitmap(bitmap);
-	                imageView.setBackgroundColor(0x00000000);
-	            }
-	        }
-	    }
+		}
+
+		@Override
+		protected void onPostExecute(Bitmap bitmap) {
+			if (imageViewReference != null && bitmap != null) {
+				final ImageView imageView = imageViewReference.get();
+				if (imageView != null) {
+					imageView.setImageBitmap(bitmap);
+					imageView.setBackgroundColor(0x00000000);
+				}
+			}
+		}
 	}
-	
+
 	public void loadBitmap(Message message, ImageView imageView) {
 		Bitmap bm;
 		try {
-			bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true);
+			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
+					(int) (metrics.density * 288), true);
 		} catch (FileNotFoundException e) {
 			bm = null;
 		}
-		if (bm!=null) {
+		if (bm != null) {
 			imageView.setImageBitmap(bm);
 			imageView.setBackgroundColor(0x00000000);
 		} else {
-		    if (cancelPotentialWork(message, imageView)) {
-		    	imageView.setBackgroundColor(0xff333333);
-		        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
-		        final AsyncDrawable asyncDrawable =
-		                new AsyncDrawable(getResources(), null, task);
-		        imageView.setImageDrawable(asyncDrawable);
-		        task.execute(message);
-		    }
+			if (cancelPotentialWork(message, imageView)) {
+				imageView.setBackgroundColor(0xff333333);
+				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+				final AsyncDrawable asyncDrawable = new AsyncDrawable(
+						getResources(), null, task);
+				imageView.setImageDrawable(asyncDrawable);
+				task.execute(message);
+			}
 		}
 	}
-	
-	public static boolean cancelPotentialWork(Message message, ImageView imageView) {
-	    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
-
-	    if (bitmapWorkerTask != null) {
-	        final Message oldMessage = bitmapWorkerTask.message;
-	        if (oldMessage == null || message != oldMessage) {
-	            bitmapWorkerTask.cancel(true);
-	        } else {
-	            return false;
-	        }
-	    }
-	    return true;
+
+	public static boolean cancelPotentialWork(Message message,
+			ImageView imageView) {
+		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+		if (bitmapWorkerTask != null) {
+			final Message oldMessage = bitmapWorkerTask.message;
+			if (oldMessage == null || message != oldMessage) {
+				bitmapWorkerTask.cancel(true);
+			} else {
+				return false;
+			}
+		}
+		return true;
 	}
-	
+
 	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
-		   if (imageView != null) {
-		       final Drawable drawable = imageView.getDrawable();
-		       if (drawable instanceof AsyncDrawable) {
-		           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
-		           return asyncDrawable.getBitmapWorkerTask();
-		       }
-		    }
-		    return null;
+		if (imageView != null) {
+			final Drawable drawable = imageView.getDrawable();
+			if (drawable instanceof AsyncDrawable) {
+				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+				return asyncDrawable.getBitmapWorkerTask();
+			}
+		}
+		return null;
 	}
-	
+
 	static class AsyncDrawable extends BitmapDrawable {
-	    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
-
-	    public AsyncDrawable(Resources res, Bitmap bitmap,
-	            BitmapWorkerTask bitmapWorkerTask) {
-	        super(res, bitmap);
-	        bitmapWorkerTaskReference =
-	            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
-	    }
-
-	    public BitmapWorkerTask getBitmapWorkerTask() {
-	        return bitmapWorkerTaskReference.get();
-	    }
+		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+		public AsyncDrawable(Resources res, Bitmap bitmap,
+				BitmapWorkerTask bitmapWorkerTask) {
+			super(res, bitmap);
+			bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
+					bitmapWorkerTask);
+		}
+
+		public BitmapWorkerTask getBitmapWorkerTask() {
+			return bitmapWorkerTaskReference.get();
+		}
 	}
 
 	public void encryptTextMessage() {
-		xmppConnectionService.getPgpEngine().encrypt(this.pendingMessage, new UiCallback() {
+		xmppConnectionService.getPgpEngine().encrypt(this.pendingMessage,
+				new UiCallback() {
 
 					@Override
-					public void userInputRequried(
-							PendingIntent pi) {
-						activity.runIntent(
-								pi,
+					public void userInputRequried(PendingIntent pi) {
+						activity.runIntent(pi,
 								ConversationActivity.REQUEST_SEND_MESSAGE);
 					}
 

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

@@ -107,7 +107,9 @@ public class ConversationFragment extends Fragment {
 
 	private LinearLayout pgpInfo;
 	private LinearLayout mucError;
+	public LinearLayout lastSeen;
 	private TextView mucErrorText;
+	private TextView lastSeenText;
 	private OnClickListener clickToMuc = new OnClickListener() {
 
 		@Override
@@ -161,6 +163,8 @@ public class ConversationFragment extends Fragment {
 		mucError = (LinearLayout) view.findViewById(R.id.muc_error);
 		mucError.setOnClickListener(clickToMuc);
 		mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg);
+		lastSeen = (LinearLayout) view.findViewById(R.id.last_seen);
+		lastSeenText = (TextView) view.findViewById(R.id.last_seen_text);
 
 		messagesView = (ListView) view.findViewById(R.id.messages_view);
 
@@ -181,7 +185,7 @@ public class ConversationFragment extends Fragment {
 			public int getItemViewType(int position) {
 				if (getItem(position).getType() == Message.TYPE_STATUS) {
 					return STATUS;
-				} else 	if (getItem(position).getStatus() <= Message.STATUS_RECIEVED) {
+				} else if (getItem(position).getStatus() <= Message.STATUS_RECIEVED) {
 					return RECIEVED;
 				} else {
 					return SENT;
@@ -335,18 +339,21 @@ public class ConversationFragment extends Fragment {
 						startActivity(intent);
 					}
 				});
-				viewHolder.image.setOnLongClickListener(new OnLongClickListener() {
-					
-					@Override
-					public boolean onLongClick(View v) {
-						Intent shareIntent = new Intent();
-						shareIntent.setAction(Intent.ACTION_SEND);
-						shareIntent.putExtra(Intent.EXTRA_STREAM, ImageProvider.getContentUri(message));
-						shareIntent.setType("image/webp");
-						startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
-						return true;
-					}
-				});
+				viewHolder.image
+						.setOnLongClickListener(new OnLongClickListener() {
+
+							@Override
+							public boolean onLongClick(View v) {
+								Intent shareIntent = new Intent();
+								shareIntent.setAction(Intent.ACTION_SEND);
+								shareIntent.putExtra(Intent.EXTRA_STREAM,
+										ImageProvider.getContentUri(message));
+								shareIntent.setType("image/webp");
+								startActivity(Intent.createChooser(shareIntent,
+										getText(R.string.share_with)));
+								return true;
+							}
+						});
 			}
 
 			@Override
@@ -405,8 +412,8 @@ public class ConversationFragment extends Fragment {
 						view.setTag(viewHolder);
 						break;
 					case STATUS:
-						view = (View) inflater.inflate(
-								R.layout.message_status, null);
+						view = (View) inflater.inflate(R.layout.message_status,
+								null);
 						viewHolder.contact_picture = (ImageView) view
 								.findViewById(R.id.message_photo);
 						if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
@@ -430,7 +437,7 @@ public class ConversationFragment extends Fragment {
 				} else {
 					viewHolder = (ViewHolder) view.getTag();
 				}
-				
+
 				if (type == STATUS) {
 					return view;
 				}
@@ -577,7 +584,11 @@ public class ConversationFragment extends Fragment {
 				activity.getActionBar().setTitle(
 						conversation.getName(useSubject));
 				activity.invalidateOptionsMenu();
-
+				if (activity.showLastseen()) {
+					lastSeen.setVisibility(View.VISIBLE);
+				}
+			} else {
+				lastSeen.setVisibility(View.GONE);
 			}
 		}
 		if (conversation.getMode() == Conversation.MODE_MULTI) {
@@ -653,6 +664,16 @@ public class ConversationFragment extends Fragment {
 					break;
 				}
 			}
+			if (activity.showLastseen()) {
+				Contact contact = conversation.getContact();
+				if ((contact.lastseen.presence != null)&&(contact.lastseen.time != 0)) {
+					lastSeenText.setText(getString(R.string.last_seen,
+						UIHelper.lastseen(getActivity(), contact.lastseen.time),
+						contact.lastseen.presence));
+				} else {
+					lastSeenText.setText(R.string.never_seen);
+				}
+			}
 			this.messageList.clear();
 			this.messageList.addAll(this.conversation.getMessages());
 			updateStatusMessages();
@@ -685,15 +706,16 @@ public class ConversationFragment extends Fragment {
 			}
 		}
 	}
-	
+
 	protected void updateStatusMessages() {
 		if (conversation.getMode() == Conversation.MODE_SINGLE) {
-			for(int i = this.messageList.size() - 1; i >= 0; --i) {
+			for (int i = this.messageList.size() - 1; i >= 0; --i) {
 				if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) {
 					return;
 				} else {
 					if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
-						this.messageList.add(i+1, Message.createStatusMessage(conversation));
+						this.messageList.add(i + 1,
+								Message.createStatusMessage(conversation));
 						return;
 					}
 				}

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

@@ -70,6 +70,19 @@ public class UIHelper {
 			return sdf.format(date);
 		}
 	}
+	
+	public static String lastseen(Context context, long time) {
+		long difference = (System.currentTimeMillis() - time) / 1000;
+		if (difference < 60) {
+			return context.getString(R.string.just_now);
+		} else if (difference < 60 * 60) {
+			return difference / 60 + " " + context.getString(R.string.mins);
+		} else if (difference < 60 * 60 * 24) {
+			return difference / (60 * 60)+ " " + context.getString(R.string.hours);
+		} else {
+			return "days";
+		}
+	}
 
 	public static int getRealPx(int dp, Context context) {
 		final DisplayMetrics metrics = context.getResources().getDisplayMetrics();