opt-in to send last userinteraction in presence

Daniel Gultsch created

Change summary

docs/XEPs.md                                                             |  1 
src/main/java/eu/siacs/conversations/entities/Contact.java               | 55 
src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java    |  7 
src/main/java/eu/siacs/conversations/parser/AbstractParser.java          | 41 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           |  8 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java          | 15 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 40 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java      | 22 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java            |  3 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                |  2 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                 |  8 
src/main/java/eu/siacs/conversations/utils/Xmlns.java                    |  2 
src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java     |  2 
src/main/res/layout/activity_contact_details.xml                         | 10 
src/main/res/values/strings.xml                                          |  3 
src/main/res/xml/preferences.xml                                         | 17 
16 files changed, 165 insertions(+), 71 deletions(-)

Detailed changes

docs/XEPs.md 🔗

@@ -19,6 +19,7 @@
 * XEP-0280: Message Carbons
 * XEP-0308: Last Message Correction
 * XEP-0313: Message Archive Management
+* XEP-0319: Last User Interaction in Presence
 * XEP-0333: Chat Markers
 * XEP-0352: Client State Indication
 * XEP-0357: Push Notifications

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

@@ -34,7 +34,6 @@ public class Contact implements ListItem, Blockable {
 	public static final String LAST_PRESENCE = "last_presence";
 	public static final String LAST_TIME = "last_time";
 	public static final String GROUPS = "groups";
-	public Lastseen lastseen = new Lastseen();
 	protected String accountUuid;
 	protected String systemName;
 	protected String serverName;
@@ -50,9 +49,14 @@ public class Contact implements ListItem, Blockable {
 	protected Account account;
 	protected Avatar avatar;
 
+	private boolean mActive = false;
+	private long mLastseen = 0;
+	private String mLastPresence = null;
+
 	public Contact(final String account, final String systemName, final String serverName,
 			final Jid jid, final int subscription, final String photoUri,
-			final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) {
+			final String systemAccount, final String keys, final String avatar, final long lastseen,
+				   final String presence, final String groups) {
 		this.accountUuid = account;
 		this.systemName = systemName;
 		this.serverName = serverName;
@@ -75,7 +79,8 @@ public class Contact implements ListItem, Blockable {
 		} catch (JSONException e) {
 			this.groups = new JSONArray();
 		}
-		this.lastseen = lastseen;
+		this.mLastseen = lastseen;
+		this.mLastPresence = presence;
 	}
 
 	public Contact(final Jid jid) {
@@ -83,9 +88,6 @@ public class Contact implements ListItem, Blockable {
 	}
 
 	public static Contact fromCursor(final Cursor cursor) {
-		final Lastseen lastseen = new Lastseen(
-				cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
-				cursor.getLong(cursor.getColumnIndex(LAST_TIME)));
 		final Jid jid;
 		try {
 			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
@@ -102,7 +104,8 @@ public class Contact implements ListItem, Blockable {
 				cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
 				cursor.getString(cursor.getColumnIndex(KEYS)),
 				cursor.getString(cursor.getColumnIndex(AVATAR)),
-				lastseen,
+				cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
+				cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
 				cursor.getString(cursor.getColumnIndex(GROUPS)));
 	}
 
@@ -197,8 +200,8 @@ public class Contact implements ListItem, Blockable {
 			values.put(PHOTOURI, photoUri);
 			values.put(KEYS, keys.toString());
 			values.put(AVATAR, avatar == null ? null : avatar.getFilename());
-			values.put(LAST_PRESENCE, lastseen.presence);
-			values.put(LAST_TIME, lastseen.time);
+			values.put(LAST_PRESENCE, mLastPresence);
+			values.put(LAST_TIME, mLastseen);
 			values.put(GROUPS, groups.toString());
 			return values;
 		}
@@ -517,18 +520,32 @@ public class Contact implements ListItem, Blockable {
 		this.commonName = cn;
 	}
 
-	public static class Lastseen {
-		public long time;
-		public String presence;
+	public void flagActive() {
+		this.mActive = true;
+	}
 
-		public Lastseen() {
-			this(null, 0);
-		}
+	public void flagInactive() {
+		this.mActive = false;
+	}
 
-		public Lastseen(final String presence, final long time) {
-			this.presence = presence;
-			this.time = time;
-		}
+	public boolean isActive() {
+		return this.mActive;
+	}
+
+	public void setLastseen(long timestamp) {
+		this.mLastseen = Math.max(timestamp, mLastseen);
+	}
+
+	public long getLastseen() {
+		return this.mLastseen;
+	}
+
+	public void setLastPresence(String presence) {
+		this.mLastPresence = presence;
+	}
+
+	public String getLastPresence() {
+		return this.mLastPresence;
 	}
 
 	public final class Options {

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

@@ -16,8 +16,6 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.PhoneHelper;
-import eu.siacs.conversations.xmpp.jid.Jid;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public abstract class AbstractGenerator {
 	private final String[] FEATURES = {
@@ -33,7 +31,8 @@ public abstract class AbstractGenerator {
 			"http://jabber.org/protocol/nick+notify",
 			"urn:xmpp:ping",
 			"jabber:iq:version",
-			"http://jabber.org/protocol/chatstates"};
+			"http://jabber.org/protocol/chatstates"
+	};
 	private final String[] MESSAGE_CONFIRMATION_FEATURES = {
 			"urn:xmpp:chat-markers:0",
 			"urn:xmpp:receipts"
@@ -45,7 +44,7 @@ public abstract class AbstractGenerator {
 	protected final String IDENTITY_NAME = "Conversations";
 	protected final String IDENTITY_TYPE = "phone";
 
-	private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+	private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 
 	protected XmppConnectionService mXmppConnectionService;
 

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

@@ -1,9 +1,7 @@
 package eu.siacs.conversations.parser;
 
-
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
-import java.util.Date;
 import java.util.Locale;
 
 import eu.siacs.conversations.entities.Account;
@@ -14,7 +12,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
 
 public abstract class AbstractParser {
 
@@ -24,42 +21,48 @@ public abstract class AbstractParser {
 		this.mXmppConnectionService = service;
 	}
 
-	public static Long getTimestamp(Element element, Long defaultValue) {
+	public static Long parseTimestamp(Element element, Long d) {
 		Element delay = element.findChild("delay","urn:xmpp:delay");
 		if (delay != null) {
 			String stamp = delay.getAttribute("stamp");
 			if (stamp != null) {
 				try {
-					return AbstractParser.parseTimestamp(delay.getAttribute("stamp")).getTime();
+					return AbstractParser.parseTimestamp(delay.getAttribute("stamp"));
 				} catch (ParseException e) {
-					return defaultValue;
+					return d;
 				}
 			}
 		}
-		return defaultValue;
+		return d;
 	}
 
-	protected long getTimestamp(Element packet) {
-		return getTimestamp(packet,System.currentTimeMillis());
+	public static long parseTimestamp(Element element) {
+		return parseTimestamp(element, System.currentTimeMillis());
 	}
 
-	public static Date parseTimestamp(String timestamp) throws ParseException {
+	public static long parseTimestamp(String timestamp) throws ParseException {
 		timestamp = timestamp.replace("Z", "+0000");
 		SimpleDateFormat dateFormat;
+		long ms;
+		if (timestamp.charAt(19) == '.' && timestamp.length() >= 25) {
+			String millis = timestamp.substring(19,timestamp.length() - 5);
+			try {
+				double fractions = Double.parseDouble("0" + millis);
+				ms = Math.round(1000 * fractions);
+			} catch (NumberFormatException e) {
+				ms = 0;
+			}
+		} else {
+			ms = 0;
+		}
 		timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length());
 		dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US);
-		return dateFormat.parse(timestamp);
+		return Math.min(dateFormat.parse(timestamp).getTime()+ms, System.currentTimeMillis());
 	}
 
-	protected void updateLastseen(long timestamp, final Account account, final Jid from) {
-		final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart();
+	protected void updateLastseen(final Account account, final Jid from) {
 		final Contact contact = account.getRoster().getContact(from);
-		if (timestamp >= contact.lastseen.time) {
-			contact.lastseen.time = timestamp;
-			if (!presence.isEmpty()) {
-				contact.lastseen.presence = presence;
-			}
-		}
+		contact.setLastPresence(from.isBareJid() ? "" : from.getResourcepart());
 	}
 
 	protected String avatarData(Element items) {

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

@@ -7,12 +7,12 @@ import android.util.Pair;
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
 
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
-import java.util.Locale;
 import java.util.Set;
 import java.util.UUID;
 
@@ -32,6 +32,7 @@ import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.Xmlns;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
 import eu.siacs.conversations.xmpp.chatstate.ChatState;
@@ -328,7 +329,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 		}
 
 		if (timestamp == null) {
-			timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
+			timestamp = AbstractParser.parseTimestamp(packet);
 		}
 		final String body = packet.getBody();
 		final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
@@ -439,7 +440,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 					message.setType(Message.TYPE_PRIVATE);
 				}
 			} else {
-				updateLastseen(timestamp, account, from);
+				updateLastseen(account, from);
 			}
 
 			if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
@@ -601,7 +602,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 					mXmppConnectionService.markRead(conversation);
 				}
 			} else {
-				updateLastseen(timestamp, account, from);
 				final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), displayed.getAttribute("id"), Message.STATUS_SEND_DISPLAYED);
 				Message message = displayedMessage == null ? null : displayedMessage.prev();
 				while (message != null

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

@@ -2,6 +2,7 @@ package eu.siacs.conversations.parser;
 
 import android.util.Log;
 
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -206,6 +207,20 @@ public class PresenceParser extends AbstractParser implements
 				mXmppConnectionService.fetchCaps(account, from, presence);
 			}
 
+			final Element idle = packet.findChild("idle","urn:xmpp:idle:1");
+			if (idle != null) {
+				contact.flagInactive();
+				String since = idle.getAttribute("since");
+				try {
+					contact.setLastseen(AbstractParser.parseTimestamp(since));
+				} catch (NullPointerException | ParseException e) {
+					contact.setLastseen(System.currentTimeMillis());
+				}
+			} else {
+				contact.flagActive();
+				contact.setLastseen(AbstractParser.parseTimestamp(packet));
+			}
+
 			PgpEngine pgp = mXmppConnectionService.getPgpEngine();
 			Element x = packet.findChild("x", "jabber:x:signed");
 			if (pgp != null && x != null) {

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

@@ -20,7 +20,6 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.FileObserver;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 import android.os.SystemClock;
@@ -80,6 +79,7 @@ import eu.siacs.conversations.entities.Roster;
 import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.generator.AbstractGenerator;
 import eu.siacs.conversations.generator.IqGenerator;
 import eu.siacs.conversations.generator.MessageGenerator;
 import eu.siacs.conversations.generator.PresenceGenerator;
@@ -141,6 +141,9 @@ public class XmppConnectionService extends Service {
 	private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 	private final IqGenerator mIqGenerator = new IqGenerator(this);
 	private final List<String> mInProgressAvatarFetches = new ArrayList<>();
+
+	private long mLastActivity = 0;
+
 	public DatabaseBackend databaseBackend;
 	private ContentObserver contactObserver = new ContentObserver(null) {
 		@Override
@@ -1584,6 +1587,7 @@ public class XmppConnectionService extends Service {
 
 	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
 		synchronized (this) {
+			this.mLastActivity = System.currentTimeMillis();
 			if (checkListeners()) {
 				switchToForeground();
 			}
@@ -1796,15 +1800,21 @@ public class XmppConnectionService extends Service {
 	}
 
 	private void switchToForeground() {
+		final boolean broadcastLastActivity = broadcastLastActivity();
 		for (Conversation conversation : getConversations()) {
 			conversation.setIncomingChatState(ChatState.ACTIVE);
 		}
 		for (Account account : getAccounts()) {
 			if (account.getStatus() == Account.State.ONLINE) {
 				account.deactivateGracePeriod();
-				XmppConnection connection = account.getXmppConnection();
-				if (connection != null && connection.getFeatures().csi()) {
-					connection.sendActive();
+				final XmppConnection connection = account.getXmppConnection();
+				if (connection != null ) {
+					if (connection.getFeatures().csi()) {
+						connection.sendActive();
+					}
+					if (broadcastLastActivity) {
+						sendPresence(account, false); //send new presence but don't include idle because we are not
+					}
 				}
 			}
 		}
@@ -1812,6 +1822,7 @@ public class XmppConnectionService extends Service {
 	}
 
 	private void switchToBackground() {
+		final boolean broadcastLastActivity = broadcastLastActivity();
 		for (Account account : getAccounts()) {
 			if (account.getStatus() == Account.State.ONLINE) {
 				XmppConnection connection = account.getXmppConnection();
@@ -1819,6 +1830,9 @@ public class XmppConnectionService extends Service {
 					if (connection.getFeatures().csi()) {
 						connection.sendInactive();
 					}
+					if (broadcastLastActivity) {
+						sendPresence(account, broadcastLastActivity);
+					}
 					if (Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND && mPushManagementService.available(account)) {
 						connection.waitForPush();
 						cancelWakeUpCall(account.getUuid().hashCode());
@@ -2253,6 +2267,7 @@ public class XmppConnectionService extends Service {
 	private void disconnect(Account account, boolean force) {
 		if ((account.getStatus() == Account.State.ONLINE)
 				|| (account.getStatus() == Account.State.DISABLED)) {
+			final XmppConnection connection = account.getXmppConnection();
 			if (!force) {
 				List<Conversation> conversations = getConversations();
 				for (Conversation conversation : conversations) {
@@ -2270,7 +2285,7 @@ public class XmppConnectionService extends Service {
 				}
 				sendOfflinePresence(account);
 			}
-			account.getXmppConnection().disconnect(force);
+			connection.disconnect(force);
 		}
 	}
 
@@ -2814,6 +2829,10 @@ public class XmppConnectionService extends Service {
 		return getPreferences().getBoolean("show_connection_options", false);
 	}
 
+	public boolean broadcastLastActivity() {
+		return getPreferences().getBoolean("last_activity", false);
+	}
+
 	public int unreadCount() {
 		int count = 0;
 		for (Conversation conversation : getConversations()) {
@@ -3052,6 +3071,10 @@ public class XmppConnectionService extends Service {
 	}
 
 	public void sendPresence(final Account account) {
+		sendPresence(account, checkListeners() && broadcastLastActivity());
+	}
+
+	private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
 		PresencePacket packet;
 		if (manuallyChangePresence()) {
 			packet =  mPresenceGenerator.selfPresence(account, account.getPresenceStatus());
@@ -3062,6 +3085,10 @@ public class XmppConnectionService extends Service {
 		} else {
 			packet = mPresenceGenerator.selfPresence(account, getTargetPresence());
 		}
+		if (mLastActivity > 0 && includeIdleTimestamp) {
+			long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates
+			packet.addChild("idle","urn:xmpp:idle:1").setAttribute("since", AbstractGenerator.getTimestamp(since));
+		}
 		sendPresencePacket(account, packet);
 	}
 
@@ -3072,9 +3099,10 @@ public class XmppConnectionService extends Service {
 	}
 
 	public void refreshAllPresences() {
+		boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
 		for (Account account : getAccounts()) {
 			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
-				sendPresence(account);
+				sendPresence(account, includeIdleTimestamp);
 			}
 		}
 	}

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

@@ -104,6 +104,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
 		}
 	};
 	private Jid accountJid;
+	private TextView lastseen;
 	private Jid contactJid;
 	private TextView contactJidTv;
 	private TextView accountJidTv;
@@ -114,7 +115,8 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
 	private QuickContactBadge badge;
 	private LinearLayout keys;
 	private LinearLayout tags;
-	private boolean showDynamicTags;
+	private boolean showDynamicTags = false;
+	private boolean showLastSeen = false;
 	private String messageFingerprint;
 
 	private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
@@ -203,6 +205,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
 
 		contactJidTv = (TextView) findViewById(R.id.details_contactjid);
 		accountJidTv = (TextView) findViewById(R.id.details_account);
+		lastseen = (TextView) findViewById(R.id.details_lastseen);
 		statusMessage = (TextView) findViewById(R.id.status_message);
 		send = (CheckBox) findViewById(R.id.details_send_presence);
 		receive = (CheckBox) findViewById(R.id.details_receive_presence);
@@ -220,9 +223,14 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
 			getActionBar().setHomeButtonEnabled(true);
 			getActionBar().setDisplayHomeAsUpEnabled(true);
 		}
+	}
 
+	@Override
+	public void onStart() {
+		super.onStart();
 		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
 		this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false);
+		this.showLastSeen = preferences.getBoolean("last_activity", false);
 	}
 
 	@Override
@@ -371,6 +379,18 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
 			statusMessage.setVisibility(View.GONE);
 		}
 
+		if (contact.isBlocked() && !this.showDynamicTags) {
+			lastseen.setVisibility(View.VISIBLE);
+			lastseen.setText(R.string.contact_blocked);
+		} else {
+			if (showLastSeen && contact.getLastseen() > 0) {
+				lastseen.setVisibility(View.VISIBLE);
+				lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
+			} else {
+				lastseen.setVisibility(View.GONE);
+			}
+		}
+
 		if (contact.getPresences().size() > 1) {
 			contactJidTv.setText(contact.getDisplayJid() + " ("
 					+ contact.getPresences().size() + ")");

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

@@ -162,7 +162,8 @@ public class SettingsActivity extends XmppActivity implements
 				"away_when_screen_off",
 				"allow_message_correction",
 				"treat_vibrate_as_silent",
-				"manually_change_presence");
+				"manually_change_presence",
+				"last_activity");
 		if (name.equals("resource")) {
 			String resource = preferences.getString("resource", "mobile")
 					.toLowerCase(Locale.US);

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

@@ -912,7 +912,7 @@ public abstract class XmppActivity extends Activity {
 				final String[] presencesArray = presences.asStringArray();
 				int preselectedPresence = 0;
 				for (int i = 0; i < presencesArray.length; ++i) {
-					if (presencesArray[i].equals(contact.lastseen.presence)) {
+					if (presencesArray[i].equals(contact.getLastPresence())) {
 						preselectedPresence = i;
 						break;
 					}

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

@@ -107,12 +107,10 @@ public class UIHelper {
 			.get(Calendar.DAY_OF_YEAR);
 	}
 
-	public static String lastseen(Context context, long time) {
-		if (time == 0) {
-			return context.getString(R.string.never_seen);
-		}
+	public static String lastseen(Context context, boolean active, long time) {
 		long difference = (System.currentTimeMillis() - time) / 1000;
-		if (difference < 60) {
+		active = active && difference <= 300;
+		if (active || difference < 60) {
 			return context.getString(R.string.last_seen_now);
 		} else if (difference < 60 * 2) {
 			return context.getString(R.string.last_seen_min);

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

@@ -1,7 +1,5 @@
 package eu.siacs.conversations.utils;
 
-import eu.siacs.conversations.Config;
-
 public final class Xmlns {
 	public static final String BLOCKING = "urn:xmpp:blocking";
 	public static final String ROSTER = "jabber:iq:roster";

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

@@ -83,7 +83,7 @@ public class MessagePacket extends AbstractAcknowledgeableStanza {
 		if (packet == null) {
 			return null;
 		}
-		Long timestamp = AbstractParser.getTimestamp(forwarded,null);
+		Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
 		return new Pair(packet,timestamp);
 	}
 

src/main/res/layout/activity_contact_details.xml 🔗

@@ -53,12 +53,20 @@
                     android:orientation="horizontal">
                 </LinearLayout>
 
+                <TextView
+                    android:id="@+id/details_lastseen"
+                    android:layout_marginTop="4dp"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/black54"
+                    android:textSize="?attr/TextSizeBody" />
+
                 <TextView
                     android:layout_marginTop="8dp"
                     android:id="@+id/status_message"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:textColor="@color/black54"
+                    android:textColor="@color/black87"
                     android:textStyle="italic"
                     android:textSize="?attr/TextSizeBody" />
 

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

@@ -653,4 +653,7 @@
 	<string name="gp_short">Short</string>
 	<string name="gp_medium">Medium</string>
 	<string name="gp_long">Long</string>
+	<string name="pref_broadcast_last_activity">Broadcast last activity</string>
+	<string name="pref_broadcast_last_activity_summary">Let all your contacts know when use Conversations</string>
+	<string name="pref_privacy">Privacy</string>
 </resources>

src/main/res/xml/preferences.xml 🔗

@@ -15,6 +15,8 @@
             android:key="resource"
             android:summary="@string/pref_xmpp_resource_summary"
             android:title="@string/pref_xmpp_resource"/>
+    </PreferenceCategory>
+    <PreferenceCategory android:title="@string/pref_privacy">
         <CheckBoxPreference
             android:defaultValue="true"
             android:key="confirm_messages"
@@ -26,11 +28,13 @@
             android:key="chat_states"
             android:summary="@string/pref_chat_states_summary"
             android:title="@string/pref_chat_states"/>
-
-    </PreferenceCategory>
-    <PreferenceCategory
-        android:key="notifications"
-        android:title="@string/pref_notification_settings">
+        <CheckBoxPreference
+            android:defaultValue="false"
+            android:key="last_activity"
+            android:title="@string/pref_broadcast_last_activity"
+            android:summary="@string/pref_broadcast_last_activity_summary"/>
+        </PreferenceCategory>
+    <PreferenceCategory android:title="@string/pref_notification_settings">
         <CheckBoxPreference
             android:defaultValue="true"
             android:key="show_notification"
@@ -88,8 +92,7 @@
             android:entryValues="@array/grace_periods_values"
             />
     </PreferenceCategory>
-    <PreferenceCategory
-        android:title="@string/pref_attachments">
+    <PreferenceCategory android:title="@string/pref_attachments">
         <ListPreference
             android:defaultValue="524288"
             android:entries="@array/filesizes"