opt-in to send last userinteraction in presence

This commit is contained in:
Daniel Gultsch 2016-06-04 16:16:14 +02:00
parent 6639d0f23b
commit 71e9117176
16 changed files with 165 additions and 71 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -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() + ")");

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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";

View File

@ -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);
}

View File

@ -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" />

View File

@ -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>

View File

@ -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"