Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 94f8c6ae authored by Guillaume Jacquart's avatar Guillaume Jacquart
Browse files

feat:3185,3186,3189: register to eOS Broadcast topics on device config changes

parent a0bfa947
Loading
Loading
Loading
Loading
Loading
+31 −0
Original line number Diff line number Diff line
/*
 *  Copyright (c) 2025 E FOUNDATION
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.notificationsreceiver.repositories

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import foundation.e.notificationsreceiver.domain.repositories.SubscriptionsRepository

@InstallIn(SingletonComponent::class)
@Module
abstract class RepositoryModule {
    @Binds
    abstract fun bindSubscriptionsRepository(impl: SubscriptionsRepositoryImpl): SubscriptionsRepository
}
+81 −0
Original line number Diff line number Diff line
/*
 *  Copyright (c) 2025 E FOUNDATION
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package foundation.e.notificationsreceiver.repositories

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.notificationsreceiver.domain.entities.TopicSubscription
import foundation.e.notificationsreceiver.domain.repositories.SubscriptionsRepository
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.db.SubscriptionDao
import io.heckel.ntfy.util.randomSubscriptionId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Date
import javax.inject.Inject

class SubscriptionsRepositoryImpl @Inject constructor(
    @ApplicationContext private val appContext: Context,
) : SubscriptionsRepository {

    private val subscriptionDao: SubscriptionDao
    private val repository: Repository
    init {
        val database = Database.getInstance(appContext)
        subscriptionDao = database.subscriptionDao()
        repository = Repository.getInstance(appContext)
    }

    override fun getBaseUrl(): String {
        return repository.getDefaultBaseUrl() ?: appContext.getString(R.string.app_base_url)
    }

    override suspend fun getSubscriptions(): List<TopicSubscription> = withContext(Dispatchers.IO) {
        subscriptionDao.list().map { TopicSubscription(localId = it.id, baseUrl = it.baseUrl, topic = it.topic) }
    }

    override suspend fun subscribe(topicSubscription: TopicSubscription) = withContext(Dispatchers.IO) {
        val subscription = Subscription(
            id = randomSubscriptionId(),
            baseUrl = topicSubscription.baseUrl,
            topic = topicSubscription.topic,
            instant = true,
            dedicatedChannels = false,
            mutedUntil = 0,
            minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
            autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
            insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
            lastNotificationId = null,
            icon = null,
            upAppId = null,
            upConnectorToken = null,
            displayName = null,
            totalCount = 0,
            newCount = 0,
            lastActive = Date().time / 1000,
        )

        subscriptionDao.add(subscription)
    }

    override suspend fun unsubscribe(topicSubscription: TopicSubscription): Unit = withContext(Dispatchers.IO) {
        subscriptionDao.remove(topicSubscription.localId)
    }
}
+24 −0
Original line number Diff line number Diff line
/*
 *  Copyright (c) 2025 E FOUNDATION
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.notificationsreceiver.domain.entities

data class TopicSubscription(
    val localId: Long = -1,
    val baseUrl: String,
    val topic: String,
)
+85 −0
Original line number Diff line number Diff line
/*
 *  Copyright (c) 2025 E FOUNDATION
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.notificationsreceiver.domain.procedures

import foundation.e.notificationsreceiver.domain.bridges.androidinterfaces.DeviceConfigurationAndroidInterface
import foundation.e.notificationsreceiver.domain.entities.TopicSubscription
import foundation.e.notificationsreceiver.domain.repositories.SubscriptionsRepository
import foundation.e.notificationsreceiver.domain.utils.AppBackgroundScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class RegisterToEOSBroadcastProcedure @Inject constructor(
    private val deviceConfigurationAndroidInterface: DeviceConfigurationAndroidInterface,
    private val subscriptionRepository: SubscriptionsRepository,
    @AppBackgroundScope private val backgroundScope: CoroutineScope,
) {
    companion object {
        private const val EOS_BROADCAST_TOPIC_PREFIX = "eOS"
        private const val ALL_DEVICES_TOPIC_BASE = "all"
        private const val DEFAULT_LANG = "en"
        private val SUPPORTED_LANGUAGES = setOf("de", "en", "es", "fr", "it")
    }

    fun register() {
        backgroundScope.launch(Dispatchers.IO) {
            updateRegistration()
        }
    }

    internal suspend fun updateRegistration() {
        val toSubscribeTopics = buildTopicsList()

        val subscribed = subscriptionRepository.getSubscriptions()
            .filter { it.topic.startsWith(EOS_BROADCAST_TOPIC_PREFIX) }
        subscribed.minus(toSubscribeTopics).forEach {
            subscriptionRepository.unsubscribe(it)
        }
        (toSubscribeTopics - subscribed).forEach {
            subscriptionRepository.subscribe(it)
        }
    }

    internal suspend fun buildTopicsList(): List<TopicSubscription> {
        val topics = mutableListOf(ALL_DEVICES_TOPIC_BASE)
        val deviceName = escapeTopicElement(deviceConfigurationAndroidInterface.device)
        topics.add(deviceName)

        deviceConfigurationAndroidInterface.version?.split("-")?.get(0)?.let {
            val eOSVersion = escapeTopicElement(it)
            topics.add(eOSVersion)
            topics.add("$deviceName-$eOSVersion")
        }

        val lang = escapeTopicElement(
            deviceConfigurationAndroidInterface.getLang()
                .takeIf { it in SUPPORTED_LANGUAGES } ?: DEFAULT_LANG,
        )

        val baseUrl = subscriptionRepository.getBaseUrl()
        return topics.map { TopicSubscription(baseUrl = baseUrl, topic = "${EOS_BROADCAST_TOPIC_PREFIX}-$it-$lang") }
    }

    private fun escapeTopicElement(element: String): String {
        return element.replace(Regex("[^_A-Za-z0-9]"), "_")
    }
}
+29 −0
Original line number Diff line number Diff line
/*
 *  Copyright (c) 2025 E FOUNDATION
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.notificationsreceiver.domain.repositories

import foundation.e.notificationsreceiver.domain.entities.TopicSubscription

interface SubscriptionsRepository {
    fun getBaseUrl(): String

    suspend fun getSubscriptions(): List<TopicSubscription>

    suspend fun subscribe(topicSubscription: TopicSubscription)
    suspend fun unsubscribe(topicSubscription: TopicSubscription)
}
Loading