move non-muc presence sending into PresenceManager

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/AppSettings.java                         |  36 
src/main/java/eu/siacs/conversations/android/Device.java                      |  80 
src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java         |   5 
src/main/java/eu/siacs/conversations/services/NotificationService.java        |  14 
src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java          |  13 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      | 192 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java              |   3 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                     |   4 
src/main/java/eu/siacs/conversations/utils/PhoneHelper.java                   |  28 
src/main/java/eu/siacs/conversations/xmpp/Managers.java                       |   2 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                 |   4 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java |   4 
src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java          |   8 
src/main/java/eu/siacs/conversations/xmpp/manager/MultiUserChatManager.java   |  14 
src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java        |  97 
src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java      |   3 
16 files changed, 297 insertions(+), 210 deletions(-)

Detailed changes

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

@@ -27,13 +27,13 @@ public class AppSettings {
     public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
     public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
     public static final String BROADCAST_LAST_ACTIVITY = "last_activity";
+    public static final String SEND_CHAT_STATES = "chat_states";
     public static final String THEME = "theme";
     public static final String DYNAMIC_COLORS = "dynamic_colors";
     public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
     public static final String OMEMO = "omemo";
     public static final String ALLOW_SCREENSHOTS = "allow_screenshots";
     public static final String RINGTONE = "call_ringtone";
-    public static final String BTBV = "btbv";
 
     public static final String CONFIRM_MESSAGES = "confirm_messages";
     public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction";
@@ -57,6 +57,7 @@ public class AppSettings {
     public static final String AUTO_ACCEPT_FILE_SIZE = "auto_accept_file_size";
 
     private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers";
+    private static final String NOTIFICATIONS_FROM_STRANGERS = "notifications_from_strangers";
     private static final String INSTALLATION_ID = "im.conversations.android.install_id";
 
     private static final String EXTERNAL_STORAGE_AUTHORITY =
@@ -102,7 +103,7 @@ public class AppSettings {
     }
 
     public boolean isBTBVEnabled() {
-        return getBooleanPreference(BTBV, R.bool.btbv);
+        return getBooleanPreference(BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
     }
 
     public boolean isTrustSystemCAStore() {
@@ -145,11 +146,37 @@ public class AppSettings {
         return getBooleanPreference(BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
     }
 
+    public boolean isUserManagedAvailability() {
+        return getBooleanPreference(MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
+    }
+
+    public boolean isAutomaticAvailability() {
+        return !isUserManagedAvailability();
+    }
+
+    public boolean isDndOnSilentMode() {
+        return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
+    }
+
+    public boolean isTreatVibrateAsSilent() {
+        return getBooleanPreference(
+                AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
+    }
+
+    public boolean isAwayWhenScreenLocked() {
+        return getBooleanPreference(
+                AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
+    }
+
     public boolean isUseTor() {
         return QuickConversationsService.isConversations()
                 && getBooleanPreference(USE_TOR, R.bool.use_tor);
     }
 
+    public boolean isSendChatStates() {
+        return getBooleanPreference(SEND_CHAT_STATES, R.bool.chat_states);
+    }
+
     public boolean isExtendedConnectionOptions() {
         return QuickConversationsService.isConversations()
                 && getBooleanPreference(
@@ -161,6 +188,11 @@ public class AppSettings {
                 ACCEPT_INVITES_FROM_STRANGERS, R.bool.accept_invites_from_strangers);
     }
 
+    public boolean isNotificationsFromStrangers() {
+        return getBooleanPreference(
+                NOTIFICATIONS_FROM_STRANGERS, R.bool.notifications_from_strangers);
+    }
+
     public boolean isKeepForegroundService() {
         return Compatibility.twentySix()
                 || getBooleanPreference(KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service);

src/main/java/eu/siacs/conversations/android/Device.java 🔗

@@ -0,0 +1,80 @@
+package eu.siacs.conversations.android;
+
+import android.app.KeyguardManager;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.PowerManager;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+
+public class Device {
+
+    private final Context context;
+
+    public Device(final Context context) {
+        this.context = context;
+    }
+
+    public boolean isScreenLocked() {
+        final var keyguardManager = context.getSystemService(KeyguardManager.class);
+        final var powerManager = context.getSystemService(PowerManager.class);
+        final var locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
+        final boolean interactive;
+        try {
+            interactive = powerManager != null && powerManager.isInteractive();
+        } catch (final Exception e) {
+            return false;
+        }
+        return locked || !interactive;
+    }
+
+    public boolean isPhoneSilenced(final boolean vibrateIsSilent) {
+        final var notificationManager = context.getSystemService(NotificationManager.class);
+        final int filter =
+                notificationManager == null
+                        ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN
+                        : notificationManager.getCurrentInterruptionFilter();
+        final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+        final var audioManager = context.getSystemService(AudioManager.class);
+        final int ringerMode =
+                audioManager == null
+                        ? AudioManager.RINGER_MODE_NORMAL
+                        : audioManager.getRingerMode();
+        try {
+            if (vibrateIsSilent) {
+                return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
+            } else {
+                return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
+            }
+        } catch (final Throwable throwable) {
+            Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced", throwable);
+            return notificationDnd;
+        }
+    }
+
+    public boolean isPhysicalDevice() {
+        return !isEmulator();
+    }
+
+    private static boolean isEmulator() {
+        return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
+                || Build.FINGERPRINT.startsWith("generic")
+                || Build.FINGERPRINT.startsWith("unknown")
+                || Build.HARDWARE.contains("goldfish")
+                || Build.HARDWARE.contains("ranchu")
+                || Build.MODEL.contains("google_sdk")
+                || Build.MODEL.contains("Emulator")
+                || Build.MODEL.contains("Android SDK built for x86")
+                || Build.MANUFACTURER.contains("Genymotion")
+                || Build.PRODUCT.contains("sdk_google")
+                || Build.PRODUCT.contains("google_sdk")
+                || Build.PRODUCT.contains("sdk")
+                || Build.PRODUCT.contains("sdk_x86")
+                || Build.PRODUCT.contains("sdk_gphone64_arm64")
+                || Build.PRODUCT.contains("vbox86p")
+                || Build.PRODUCT.contains("emulator")
+                || Build.PRODUCT.contains("simulator");
+    }
+}

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

@@ -12,11 +12,6 @@ public class PresenceGenerator extends AbstractGenerator {
         super(service);
     }
 
-    public im.conversations.android.xmpp.model.stanza.Presence selfPresence(
-            Account account, Presence.Availability status) {
-        return selfPresence(account, status, true);
-    }
-
     public im.conversations.android.xmpp.model.stanza.Presence selfPresence(
             final Account account, final Presence.Availability status, final boolean personal) {
         final var connection = account.getXmppConnection();

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

@@ -50,6 +50,7 @@ import com.google.common.primitives.Ints;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.Device;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
@@ -390,11 +391,12 @@ public class NotificationService {
     }
 
     private boolean notifyMessage(final Message message) {
+        final var appSettings = new AppSettings(mXmppConnectionService.getApplicationContext());
         final Conversation conversation = (Conversation) message.getConversation();
         return message.getStatus() == Message.STATUS_RECEIVED
                 && !conversation.isMuted()
                 && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message))
-                && (!conversation.isWithStranger() || notificationsFromStrangers())
+                && (!conversation.isWithStranger() || appSettings.isNotificationsFromStrangers())
                 && message.getType() != Message.TYPE_RTP_SESSION;
     }
 
@@ -403,11 +405,6 @@ public class NotificationService {
                 && message.getStatus() == Message.STATUS_RECEIVED;
     }
 
-    public boolean notificationsFromStrangers() {
-        return mXmppConnectionService.getBooleanPreference(
-                "notifications_from_strangers", R.bool.notifications_from_strangers);
-    }
-
     public void pushFromBacklog(final Message message) {
         if (notifyMessage(message)) {
             synchronized (notifications) {
@@ -513,9 +510,8 @@ public class NotificationService {
 
     public void pushFailedDelivery(final Message message) {
         final Conversation conversation = (Conversation) message.getConversation();
-        final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked();
         if (this.mIsInForeground
-                && isScreenLocked
+                && !new Device(mXmppConnectionService).isScreenLocked()
                 && this.mOpenConversation == message.getConversation()) {
             Log.d(
                     Config.LOGTAG,
@@ -755,7 +751,7 @@ public class NotificationService {
                             + ": suppressing notification because turned off");
             return;
         }
-        final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
+        final boolean isScreenLocked = new Device(mXmppConnectionService).isScreenLocked();
         if (this.mIsInForeground
                 && !isScreenLocked
                 && this.mOpenConversation == message.getConversation()) {

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

@@ -29,8 +29,8 @@ import eu.siacs.conversations.receiver.UnifiedPushDistributor;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.stanza.Presence;
 import im.conversations.android.xmpp.model.up.Push;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
@@ -69,7 +69,10 @@ public class UnifiedPushBroker {
             if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
                 final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service);
                 if (database.hasEndpoints(transport)) {
-                    sendDirectedPresence(transportAccount, transport.transport);
+                    transportAccount
+                            .getXmppConnection()
+                            .getManager(PresenceManager.class)
+                            .available(transport.transport);
                 }
                 Log.d(
                         Config.LOGTAG,
@@ -79,12 +82,6 @@ public class UnifiedPushBroker {
         }
     }
 
-    private void sendDirectedPresence(final Account account, Jid to) {
-        final var presence = new Presence();
-        presence.setTo(to);
-        service.sendPresencePacket(account, presence);
-    }
-
     public void renewUnifiedPushEndpoints() {
         renewUnifiedPushEndpoints(null);
     }

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

@@ -5,7 +5,6 @@ import static eu.siacs.conversations.utils.Compatibility.s;
 import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.AlarmManager;
-import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -42,7 +41,6 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.util.LruCache;
 import android.util.Pair;
-import androidx.annotation.BoolRes;
 import androidx.annotation.IntegerRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -706,7 +704,7 @@ public class XmppConnectionService extends Service {
                         });
             case AudioManager.RINGER_MODE_CHANGED_ACTION:
             case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
-                if (dndOnSilentMode()) {
+                if (appSettings.isDndOnSilentMode() && appSettings.isAutomaticAvailability()) {
                     refreshAllPresences();
                 }
                 break;
@@ -714,7 +712,7 @@ public class XmppConnectionService extends Service {
                 deactivateGracePeriod();
             case Intent.ACTION_USER_PRESENT:
             case Intent.ACTION_SCREEN_OFF:
-                if (awayWhenScreenLocked()) {
+                if (appSettings.isAwayWhenScreenLocked() && appSettings.isAutomaticAvailability()) {
                     refreshAllPresences();
                 }
                 break;
@@ -1052,25 +1050,6 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    private boolean dndOnSilentMode() {
-        return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
-    }
-
-    private boolean manuallyChangePresence() {
-        return getBooleanPreference(
-                AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
-    }
-
-    private boolean treatVibrateAsSilent() {
-        return getBooleanPreference(
-                AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
-    }
-
-    private boolean awayWhenScreenLocked() {
-        return getBooleanPreference(
-                AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
-    }
-
     private String getCompressPicturesPreference() {
         return getPreferences()
                 .getString(
@@ -1078,55 +1057,6 @@ public class XmppConnectionService extends Service {
                         getResources().getString(R.string.picture_compression));
     }
 
-    private im.conversations.android.xmpp.model.stanza.Presence.Availability getTargetPresence() {
-        if (dndOnSilentMode() && isPhoneSilenced()) {
-            return im.conversations.android.xmpp.model.stanza.Presence.Availability.DND;
-        } else if (awayWhenScreenLocked() && isScreenLocked()) {
-            return im.conversations.android.xmpp.model.stanza.Presence.Availability.AWAY;
-        } else {
-            return im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE;
-        }
-    }
-
-    public boolean isScreenLocked() {
-        final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
-        final PowerManager powerManager = getSystemService(PowerManager.class);
-        final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
-        final boolean interactive;
-        try {
-            interactive = powerManager != null && powerManager.isInteractive();
-        } catch (final Exception e) {
-            return false;
-        }
-        return locked || !interactive;
-    }
-
-    private boolean isPhoneSilenced() {
-        final NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        final int filter =
-                notificationManager == null
-                        ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN
-                        : notificationManager.getCurrentInterruptionFilter();
-        final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-        final AudioManager audioManager = getSystemService(AudioManager.class);
-        final int ringerMode =
-                audioManager == null
-                        ? AudioManager.RINGER_MODE_NORMAL
-                        : audioManager.getRingerMode();
-        try {
-            if (treatVibrateAsSilent()) {
-                return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
-            } else {
-                return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
-            }
-        } catch (final Throwable throwable) {
-            Log.d(
-                    Config.LOGTAG,
-                    "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
-            return notificationDnd;
-        }
-    }
-
     private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
         Log.d(Config.LOGTAG, "resetting all attempt counts");
         for (Account account : accounts) {
@@ -1433,7 +1363,7 @@ public class XmppConnectionService extends Service {
     }
 
     public void toggleScreenEventReceiver() {
-        if (awayWhenScreenLocked() && !manuallyChangePresence()) {
+        if (appSettings.isAwayWhenScreenLocked() && appSettings.isAutomaticAvailability()) {
             final IntentFilter filter = new IntentFilter();
             filter.addAction(Intent.ACTION_SCREEN_ON);
             filter.addAction(Intent.ACTION_SCREEN_OFF);
@@ -1655,8 +1585,8 @@ public class XmppConnectionService extends Service {
         return connection;
     }
 
-    public void sendChatState(Conversation conversation) {
-        if (sendChatStates()) {
+    public void sendChatState(final Conversation conversation) {
+        if (appSettings.isSendChatStates()) {
             final var packet = mMessageGenerator.generateChatState(conversation);
             sendMessagePacket(conversation.getAccount(), packet);
         }
@@ -1848,7 +1778,7 @@ public class XmppConnectionService extends Service {
                 mMessageGenerator.addDelay(packet, message.getTimeSent());
             }
             if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
-                if (this.sendChatStates()) {
+                if (this.appSettings.isSendChatStates()) {
                     packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
                 }
             }
@@ -3112,7 +3042,7 @@ public class XmppConnectionService extends Service {
 
     private void switchToForeground() {
         toggleSoftDisabled(false);
-        final boolean broadcastLastActivity = broadcastLastActivity();
+        final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
         for (Conversation conversation : getConversations()) {
             if (conversation.getMode() == Conversation.MODE_MULTI) {
                 conversation.getMucOptions().resetChatState();
@@ -3120,45 +3050,41 @@ public class XmppConnectionService extends Service {
                 conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
             }
         }
-        for (Account account : getAccounts()) {
-            if (account.getStatus() == Account.State.ONLINE) {
-                account.deactivateGracePeriod();
-                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
-                    }
-                }
+        for (final var account : getAccounts()) {
+            if (account.getStatus() != Account.State.ONLINE) {
+                continue;
+            }
+            account.deactivateGracePeriod();
+            final XmppConnection connection = account.getXmppConnection();
+            if (connection.getFeatures().csi()) {
+                connection.sendActive();
+            }
+            if (broadcastLastActivity) {
+                // send new presence but don't include idle because we are not
+                connection.getManager(PresenceManager.class).available(false);
             }
         }
         Log.d(Config.LOGTAG, "app switched into foreground");
     }
 
     private void switchToBackground() {
-        final boolean broadcastLastActivity = broadcastLastActivity();
+        final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
         if (broadcastLastActivity) {
             mLastActivity = System.currentTimeMillis();
             final SharedPreferences.Editor editor = getPreferences().edit();
             editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
             editor.apply();
         }
-        for (Account account : getAccounts()) {
-            if (account.getStatus() == Account.State.ONLINE) {
-                XmppConnection connection = account.getXmppConnection();
-                if (connection != null) {
-                    if (broadcastLastActivity) {
-                        sendPresence(account, true);
-                    }
-                    if (connection.getFeatures().csi()) {
-                        connection.sendInactive();
-                    }
-                }
+        for (final var account : getAccounts()) {
+            if (account.getStatus() != Account.State.ONLINE) {
+                continue;
+            }
+            final var connection = account.getXmppConnection();
+            if (broadcastLastActivity) {
+                connection.getManager(PresenceManager.class).available(true);
+            }
+            if (connection.getFeatures().csi()) {
+                connection.sendInactive();
             }
         }
         this.mNotificationService.setIsInForeground(false);
@@ -4218,7 +4144,7 @@ public class XmppConnectionService extends Service {
                     }
                 }
             }
-            sendOfflinePresence(account);
+            connection.getManager(PresenceManager.class).unavailable();
         }
         connection.disconnect(force);
     }
@@ -4549,10 +4475,6 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public boolean getBooleanPreference(String name, @BoolRes int res) {
-        return getPreferences().getBoolean(name, getResources().getBoolean(res));
-    }
-
     public boolean confirmMessages() {
         return appSettings.isConfirmMessages();
     }
@@ -4561,18 +4483,10 @@ public class XmppConnectionService extends Service {
         return appSettings.isAllowMessageCorrection();
     }
 
-    public boolean sendChatStates() {
-        return getBooleanPreference("chat_states", R.bool.chat_states);
-    }
-
     public boolean useTorToConnect() {
         return appSettings.isUseTor();
     }
 
-    public boolean broadcastLastActivity() {
-        return appSettings.isBroadcastLastActivity();
-    }
-
     public int unreadCount() {
         int count = 0;
         for (Conversation conversation : getConversations()) {
@@ -5014,27 +4928,6 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void sendPresence(final Account account) {
-        sendPresence(account, checkListeners() && broadcastLastActivity());
-    }
-
-    private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
-        final im.conversations.android.xmpp.model.stanza.Presence.Availability status;
-        if (manuallyChangePresence()) {
-            status = account.getPresenceStatus();
-        } else {
-            status = getTargetPresence();
-        }
-        final var packet = mPresenceGenerator.selfPresence(account, status);
-        if (mLastActivity > 0 && includeIdleTimestamp) {
-            long since =
-                    Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
-            packet.addChild("idle", Namespace.IDLE)
-                    .setAttribute("since", AbstractGenerator.getTimestamp(since));
-        }
-        sendPresencePacket(account, packet);
-    }
-
     private void deactivateGracePeriod() {
         for (Account account : getAccounts()) {
             account.deactivateGracePeriod();
@@ -5042,10 +4935,13 @@ public class XmppConnectionService extends Service {
     }
 
     public void refreshAllPresences() {
-        boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
-        for (Account account : getAccounts()) {
+        final boolean includeIdleTimestamp =
+                checkListeners() && appSettings.isBroadcastLastActivity();
+        for (final var account : getAccounts()) {
             if (account.isConnectionEnabled()) {
-                sendPresence(account, includeIdleTimestamp);
+                account.getXmppConnection()
+                        .getManager(PresenceManager.class)
+                        .available(includeIdleTimestamp);
             }
         }
     }
@@ -5058,11 +4954,6 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    private void sendOfflinePresence(final Account account) {
-        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
-        sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
-    }
-
     public MessageGenerator getMessageGenerator() {
         return this.mMessageGenerator;
     }
@@ -5247,7 +5138,8 @@ public class XmppConnectionService extends Service {
         return mPushManagementService;
     }
 
-    public void changeStatus(Account account, PresenceTemplate template, String signature) {
+    public void changeStatus(
+            final Account account, final PresenceTemplate template, final String signature) {
         if (!template.getStatusMessage().isEmpty()) {
             databaseBackend.insertPresenceTemplate(template);
         }
@@ -5255,7 +5147,7 @@ public class XmppConnectionService extends Service {
         account.setPresenceStatus(template.getStatus());
         account.setPresenceStatusMessage(template.getStatusMessage());
         databaseBackend.updateAccount(account);
-        sendPresence(account);
+        account.getXmppConnection().getManager(PresenceManager.class).available();
     }
 
     public List<PresenceTemplate> getPresenceTemplates(Account account) {
@@ -5345,6 +5237,10 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    public long getLastActivity() {
+        return this.mLastActivity;
+    }
+
     public interface OnMamPreferencesFetched {
         void onPreferencesFetched(Element prefs);
 

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

@@ -82,6 +82,7 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.XmppConnection.Features;
 import eu.siacs.conversations.xmpp.manager.CarbonsManager;
 import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import eu.siacs.conversations.xmpp.manager.RegistrationManager;
 import im.conversations.android.xmpp.model.data.Data;
 import im.conversations.android.xmpp.model.stanza.Presence;
@@ -1518,7 +1519,7 @@ public class EditAccountActivity extends OmemoActivity
                     mAccount.setPgpSignId(0);
                     mAccount.unsetPgpSignature();
                     xmppConnectionService.databaseBackend.updateAccount(mAccount);
-                    xmppConnectionService.sendPresence(mAccount);
+                    mAccount.getXmppConnection().getManager(PresenceManager.class).available();
                     refreshUiReal();
                 });
         builder.create().show();

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

@@ -828,7 +828,9 @@ public abstract class XmppActivity extends ActionBarActivity {
                                 public void success(String signature) {
                                     account.setPgpSignature(signature);
                                     xmppConnectionService.databaseBackend.updateAccount(account);
-                                    xmppConnectionService.sendPresence(account);
+                                    account.getXmppConnection()
+                                            .getManager(PresenceManager.class)
+                                            .available();
                                     if (conversation != null) {
                                         conversation.setNextEncryption(Message.ENCRYPTION_PGP);
                                         xmppConnectionService.updateConversation(conversation);

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

@@ -6,12 +6,9 @@ import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Build;
 import android.provider.ContactsContract.Profile;
 import android.provider.Settings;
-
 import com.google.common.base.Strings;
-
 import eu.siacs.conversations.services.QuickConversationsService;
 
 public class PhoneHelper {
@@ -23,9 +20,8 @@ public class PhoneHelper {
 
     public static Uri getProfilePictureUri(final Context context) {
         if (!QuickConversationsService.isContactListIntegration(context)
-                || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-                        && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
-                                != PackageManager.PERMISSION_GRANTED)) {
+                || context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                        != PackageManager.PERMISSION_GRANTED) {
             return null;
         }
         final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI};
@@ -42,24 +38,4 @@ public class PhoneHelper {
         }
         return null;
     }
-
-    public static boolean isEmulator() {
-        return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
-                || Build.FINGERPRINT.startsWith("generic")
-                || Build.FINGERPRINT.startsWith("unknown")
-                || Build.HARDWARE.contains("goldfish")
-                || Build.HARDWARE.contains("ranchu")
-                || Build.MODEL.contains("google_sdk")
-                || Build.MODEL.contains("Emulator")
-                || Build.MODEL.contains("Android SDK built for x86")
-                || Build.MANUFACTURER.contains("Genymotion")
-                || Build.PRODUCT.contains("sdk_google")
-                || Build.PRODUCT.contains("google_sdk")
-                || Build.PRODUCT.contains("sdk")
-                || Build.PRODUCT.contains("sdk_x86")
-                || Build.PRODUCT.contains("sdk_gphone64_arm64")
-                || Build.PRODUCT.contains("vbox86p")
-                || Build.PRODUCT.contains("emulator")
-                || Build.PRODUCT.contains("simulator");
-    }
 }

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

@@ -14,6 +14,7 @@ import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
 import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
 import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
 import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
 import eu.siacs.conversations.xmpp.manager.NickManager;
 import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager;
 import eu.siacs.conversations.xmpp.manager.PepManager;
@@ -48,6 +49,7 @@ public class Managers {
                 .put(
                         MessageDisplayedSynchronizationManager.class,
                         new MessageDisplayedSynchronizationManager(context, connection))
+                .put(MultiUserChatManager.class, new MultiUserChatManager(context, connection))
                 .put(NickManager.class, new NickManager(context, connection))
                 .put(OfflineMessagesManager.class, new OfflineMessagesManager(context, connection))
                 .put(PepManager.class, new PepManager(context, connection))

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

@@ -31,6 +31,7 @@ import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.Device;
 import eu.siacs.conversations.crypto.PgpDecryptionService;
 import eu.siacs.conversations.crypto.XmppDomainVerifier;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -54,7 +55,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.utils.Resolver;
 import eu.siacs.conversations.utils.SSLSockets;
 import eu.siacs.conversations.utils.SocksSocketFactory;
@@ -1799,7 +1799,7 @@ public class XmppConnection implements Runnable {
                                         account, appSettings.getInstallationId())));
         userAgent.setSoftware(
                 String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
-        if (!PhoneHelper.isEmulator()) {
+        if (new Device(mXmppConnectionService).isPhysicalDevice()) {
             userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
         }
         // do not include bind if 'inlineStreamManagement' is missing and we have a streamId

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

@@ -13,6 +13,7 @@ import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableSet;
+import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
@@ -249,7 +250,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
     private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) {
         final boolean notifyForStrangers =
-                mXmppConnectionService.getNotificationService().notificationsFromStrangers();
+                new AppSettings(mXmppConnectionService.getApplicationContext())
+                        .isNotificationsFromStrangers();
         if (notifyForStrangers) {
             return false;
         }

src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java 🔗

@@ -28,13 +28,13 @@ import com.google.common.util.concurrent.SettableFuture;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.Device;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.Compatibility;
-import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
@@ -622,7 +622,9 @@ public class AvatarManager extends AbstractManager {
                     resizeAndStoreAvatarAsync(
                             image, Config.AVATAR_FULL_SIZE, ImageFormat.JPEG, autoAcceptFileSize);
 
-            if (Compatibility.twentyEight() && !PhoneHelper.isEmulator()) {
+            final var device = new Device(context);
+
+            if (Compatibility.twentyEight() && device.isPhysicalDevice()) {
                 final var avatarHeifFuture =
                         resizeAndStoreAvatarAsync(
                                 image,
@@ -634,7 +636,7 @@ public class AvatarManager extends AbstractManager {
                                 avatarHeifFuture, this::upload, MoreExecutors.directExecutor());
                 avatarFutures.add(avatarHeifWithUrlFuture);
             }
-            if (Compatibility.thirtyFour() && !PhoneHelper.isEmulator()) {
+            if (Compatibility.thirtyFour() && device.isPhysicalDevice()) {
                 final var avatarAvifFuture =
                         resizeAndStoreAvatarAsync(
                                 image,

src/main/java/eu/siacs/conversations/xmpp/manager/MultiUserChatManager.java 🔗

@@ -0,0 +1,14 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+public class MultiUserChatManager extends AbstractManager {
+
+    private final XmppConnectionService service;
+
+    public MultiUserChatManager(final XmppConnectionService service, XmppConnection connection) {
+        super(service.getApplicationContext(), connection);
+        this.service = service;
+    }
+}

src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java 🔗

@@ -1,12 +1,19 @@
 package eu.siacs.conversations.xmpp.manager;
 
-import android.content.Context;
+import android.util.Log;
 import com.google.common.base.Strings;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.Device;
+import eu.siacs.conversations.generator.AbstractGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import im.conversations.android.xmpp.EntityCapabilities;
 import im.conversations.android.xmpp.EntityCapabilities2;
 import im.conversations.android.xmpp.ServiceDescription;
+import im.conversations.android.xmpp.model.Extension;
 import im.conversations.android.xmpp.model.capabilties.Capabilities;
 import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities;
 import im.conversations.android.xmpp.model.nick.Nick;
@@ -18,11 +25,16 @@ import java.util.Map;
 
 public class PresenceManager extends AbstractManager {
 
+    private final XmppConnectionService service;
+    private final AppSettings appSettings;
+
     private final Map<EntityCapabilities.Hash, ServiceDescription> serviceDescriptions =
             new HashMap<>();
 
-    public PresenceManager(Context context, XmppConnection connection) {
-        super(context, connection);
+    public PresenceManager(final XmppConnectionService service, final XmppConnection connection) {
+        super(service.getApplicationContext(), connection);
+        this.appSettings = new AppSettings(service.getApplicationContext());
+        this.service = service;
     }
 
     public void subscribe(final Jid address) {
@@ -62,6 +74,85 @@ public class PresenceManager extends AbstractManager {
         this.connection.sendPresencePacket(presence);
     }
 
+    public void available() {
+        available(service.checkListeners() && appSettings.isBroadcastLastActivity());
+    }
+
+    public void available(final boolean withIdle) {
+        final var account = connection.getAccount();
+        final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription();
+        final var infoQuery = serviceDiscoveryFeatures.asInfoQuery();
+        final var capsHash = EntityCapabilities.hash(infoQuery);
+        final var caps2Hash = EntityCapabilities2.hash(infoQuery);
+        serviceDescriptions.put(capsHash, serviceDiscoveryFeatures);
+        serviceDescriptions.put(caps2Hash, serviceDiscoveryFeatures);
+        final var capabilities = new Capabilities();
+        capabilities.setHash(caps2Hash);
+        final var legacyCapabilities = new LegacyCapabilities();
+        legacyCapabilities.setNode(DiscoManager.CAPABILITY_NODE);
+        legacyCapabilities.setHash(capsHash);
+        final var presence = new Presence();
+        presence.addExtension(capabilities);
+        presence.addExtension(legacyCapabilities);
+        final String pgpSignature = account.getPgpSignature();
+        final String message = account.getPresenceStatusMessage();
+        final Presence.Availability availability;
+        if (appSettings.isUserManagedAvailability()) {
+            availability = account.getPresenceStatus();
+        } else {
+            availability = getTargetPresence();
+        }
+        presence.setAvailability(availability);
+        presence.setStatus(message);
+        if (pgpSignature != null) {
+            final var signed = new Signed();
+            signed.setContent(pgpSignature);
+            presence.addExtension(signed);
+        }
+
+        final var lastActivity = service.getLastActivity();
+        if (lastActivity > 0 && withIdle) {
+            final long since =
+                    Math.min(lastActivity, System.currentTimeMillis()); // don't send future dates
+            presence.addChild("idle", Namespace.IDLE)
+                    .setAttribute("since", AbstractGenerator.getTimestamp(since));
+        }
+        Log.d(Config.LOGTAG, "--> " + presence);
+        connection.sendPresencePacket(presence);
+    }
+
+    public void unavailable() {
+        var presence = new Presence(Presence.Type.UNAVAILABLE);
+        this.connection.sendPresencePacket(presence);
+    }
+
+    public void available(final Jid to, final Extension... extensions) {
+        final var presence = new Presence();
+        presence.setTo(to);
+        for (final var extension : extensions) {
+            presence.addExtension(extension);
+        }
+        connection.sendPresencePacket(presence);
+    }
+
+    public void unavailable(final Jid to) {
+        final var presence = new Presence(Presence.Type.UNAVAILABLE);
+        presence.setTo(to);
+        connection.sendPresencePacket(presence);
+    }
+
+    private im.conversations.android.xmpp.model.stanza.Presence.Availability getTargetPresence() {
+        final var device = new Device(context);
+        if (appSettings.isDndOnSilentMode()
+                && device.isPhoneSilenced(appSettings.isTreatVibrateAsSilent())) {
+            return im.conversations.android.xmpp.model.stanza.Presence.Availability.DND;
+        } else if (appSettings.isAwayWhenScreenLocked() && device.isScreenLocked()) {
+            return im.conversations.android.xmpp.model.stanza.Presence.Availability.AWAY;
+        } else {
+            return im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE;
+        }
+    }
+
     public Presence getPresence(final Presence.Availability availability, final boolean personal) {
         final var account = connection.getAccount();
         final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription();

src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java 🔗

@@ -16,6 +16,7 @@ import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
 import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
 import eu.siacs.conversations.xmpp.manager.NickManager;
 import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import eu.siacs.conversations.xmpp.manager.PrivateStorageManager;
 import eu.siacs.conversations.xmpp.manager.RosterManager;
 
@@ -115,7 +116,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable {
         } else {
             trackOfflineMessageRetrieval = true;
         }
-        service.sendPresence(account);
+        getManager(PresenceManager.class).available();
         connection.trackOfflineMessageRetrieval(trackOfflineMessageRetrieval);
         if (service.getPushManagementService().available(account)) {
             service.getPushManagementService().registerPushTokenOnServer(account);