diff --git a/app/src/main/java/org/futo/circles/feature/notifications/UnifiedPushHelper.kt b/app/src/main/java/org/futo/circles/feature/notifications/UnifiedPushHelper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e884a81d3e02345bba19fcf2df5facd0289f6929
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/feature/notifications/UnifiedPushHelper.kt
@@ -0,0 +1,139 @@
+package org.futo.circles.feature.notifications
+
+import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.google.gson.Gson
+import kotlinx.coroutines.launch
+import org.futo.circles.R
+import org.futo.circles.feature.notifications.model.DiscoveryResponse
+import org.futo.circles.provider.MatrixInstanceProvider
+import org.futo.circles.provider.PreferencesProvider
+import org.matrix.android.sdk.api.cache.CacheStrategy
+import org.unifiedpush.android.connector.UnifiedPush
+import java.net.URL
+
+
+class UnifiedPushHelper(
+    private val context: Context,
+    private val preferencesProvider: PreferencesProvider,
+    private val fcmHelper: FcmHelper,
+) {
+
+    fun register(
+        activity: AppCompatActivity,
+        onDoneRunnable: Runnable? = null,
+    ) {
+        registerInternal(
+            activity,
+            onDoneRunnable = onDoneRunnable
+        )
+    }
+
+    private fun registerInternal(
+        activity: AppCompatActivity,
+        force: Boolean = false,
+        pushersManager: PushersManager? = null,
+        onDoneRunnable: Runnable? = null
+    ) {
+        activity.lifecycleScope.launch {
+            if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
+                UnifiedPush.saveDistributor(context, context.packageName)
+                UnifiedPush.registerApp(context)
+                onDoneRunnable?.run()
+                return@launch
+            }
+            if (force) {
+                // Un-register first
+                unregister(pushersManager)
+            }
+            // the !force should not be needed
+            if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) {
+                UnifiedPush.registerApp(context)
+                onDoneRunnable?.run()
+                return@launch
+            }
+
+            val distributors = UnifiedPush.getDistributors(context)
+
+            if (!force && distributors.size == 1) {
+                UnifiedPush.saveDistributor(context, distributors.first())
+                UnifiedPush.registerApp(context)
+                onDoneRunnable?.run()
+            } else {
+                openDistributorDialogInternal(
+                    activity = activity,
+                    onDoneRunnable = onDoneRunnable,
+                    distributors = distributors
+                )
+            }
+        }
+    }
+
+
+    suspend fun unregister(pushersManager: PushersManager? = null) {
+        val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
+        vectorPreferences.setFdroidSyncBackgroundMode(mode)
+        try {
+            getEndpointOrToken()?.let { pushersManager?.unregisterPusher(it) }
+        } catch (ignore: Exception) {
+        }
+
+        preferencesProvider.storeUnifiedPushEndpoint(null)
+        preferencesProvider.storePushGateway(null)
+        UnifiedPush.unregisterApp(context)
+    }
+
+
+    suspend fun storeCustomOrDefaultGateway(
+        endpoint: String,
+        onDoneRunnable: Runnable? = null
+    ) {
+        if (UnifiedPush.getDistributor(context) == context.packageName) {
+            preferencesProvider.storePushGateway(context.getString(R.string.pusher_http_url))
+            onDoneRunnable?.run()
+            return
+        }
+
+        val gateway = context.getString(R.string.default_push_gateway_http_url)
+        val parsed = URL(endpoint)
+        val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify"
+
+        try {
+            val response =
+                MatrixInstanceProvider.matrix.rawService().getUrl(custom, CacheStrategy.NoCache)
+            val discoveryResponse = Gson().fromJson(response, DiscoveryResponse::class.java)
+            if (discoveryResponse.unifiedpush.gateway == "matrix") {
+                preferencesProvider.storePushGateway(custom)
+                onDoneRunnable?.run()
+                return
+            }
+        } catch (ignore: Throwable) {
+        }
+        preferencesProvider.storePushGateway(gateway)
+        onDoneRunnable?.run()
+    }
+
+    fun isEmbeddedDistributor(): Boolean {
+        return isInternalDistributor() && fcmHelper.isFirebaseAvailable()
+    }
+
+    fun isBackgroundSync(): Boolean {
+        return isInternalDistributor() && !fcmHelper.isFirebaseAvailable()
+    }
+
+    private fun isInternalDistributor(): Boolean {
+        return UnifiedPush.getDistributor(context).isEmpty() ||
+                UnifiedPush.getDistributor(context) == context.packageName
+    }
+
+    fun getEndpointOrToken(): String? {
+        return if (isEmbeddedDistributor()) fcmHelper.getFcmToken()
+        else preferencesProvider.getUnifiedPushEndpoint()
+    }
+
+    fun getPushGateway(): String? {
+        return if (isEmbeddedDistributor()) context.getString(R.string.pusher_http_url)
+        else preferencesProvider.getPushGateway()
+    }
+}