Compare commits

...

7 Commits

Author SHA1 Message Date
Arnau Mora
6d478deefc Merge branch 'main-ose' into 1737-enable-push-by-default 2026-02-02 09:29:02 +00:00
Arnau Mora
0362c72a11 Merge branch 'main-ose' into 1737-enable-push-by-default 2026-01-29 12:01:12 +00:00
Arnau Mora Gras
7cd161f89d Create PushDistributorManager
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
2026-01-29 09:37:26 +01:00
Arnau Mora Gras
e61d4b9006 Optimize imports
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
2026-01-29 09:27:01 +01:00
Arnau Mora Gras
5b58facb41 Merge branch 'main-ose' into 1737-enable-push-by-default
# Conflicts:
#	app/src/ose/kotlin/at/bitfire/davdroid/di/OseFlavorModule.kt
2026-01-29 09:25:41 +01:00
Arnau Mora
425478baa8 Merge branch 'main-ose' into 1737-enable-push-by-default 2025-12-18 09:42:24 +00:00
Arnau Mora
6e0cde71aa Added automatic push distributor selection 2025-12-11 15:33:06 +01:00
6 changed files with 135 additions and 33 deletions

View File

@@ -0,0 +1,78 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.push
import android.content.Context
import at.bitfire.davdroid.push.PushRegistrationManager.DistributorPreferences
import at.bitfire.davdroid.settings.Settings.EXPLICIT_PUSH_DISABLE
import at.bitfire.davdroid.settings.SettingsManager
import dagger.hilt.android.qualifiers.ApplicationContext
import org.unifiedpush.android.connector.UnifiedPush
import java.util.logging.Logger
import javax.inject.Inject
class PushDistributorManager @Inject constructor(
@ApplicationContext private val context: Context,
private val logger: Logger,
private val settings: SettingsManager,
private val distributorPreferences: DistributorPreferences,
) {
/**
* Get the distributor registered by the user.
* @return The distributor package name if any, else `null`.
*/
fun getCurrentDistributor() = UnifiedPush.getSavedDistributor(context)
/**
* Get a list of available distributors installed on the system.
* @return The list of distributor's package name.
*/
fun getDistributors() = UnifiedPush.getDistributors(context)
/**
* Sets or removes (disable push) the distributor.
*
* @param pushDistributor new distributor or `null` to disable Push
*/
fun setPushDistributor(pushDistributor: String?) {
// Disable UnifiedPush and remove all subscriptions
UnifiedPush.removeDistributor(context)
update()
if (pushDistributor != null) {
// If a distributor was passed, store it and create/register subscriptions
UnifiedPush.saveDistributor(context, pushDistributor)
update()
}
}
/**
* Makes sure a distributor is selected if Push is enabled.
*
* Uses preferences from [distributorPreferences].
*/
fun update() {
val currentDistributor = getCurrentDistributor()
val isPushDisabled = settings.getBooleanOrNull(EXPLICIT_PUSH_DISABLE)
if (currentDistributor == null) {
if (isPushDisabled == true) {
logger.info("Push is explicitly disabled, no distributor will be selected.")
} else {
val availableDistributors = getDistributors()
if (availableDistributors.isNotEmpty()) {
logger.fine("No Push distributor selected, but ${availableDistributors.size} distributors are available.")
// select preferred distributor if available, otherwise first available
val distributor = distributorPreferences.packageNames.firstNotNullOfOrNull { preferredPackageName ->
availableDistributors.find { it == preferredPackageName }
} ?: availableDistributors.first()
logger.fine("Automatically selecting Push distributor: $distributor")
UnifiedPush.saveDistributor(context, distributor)
} else {
logger.fine("No Push distributor selected and no distributors are available.")
}
}
}
}
}

View File

@@ -22,7 +22,6 @@ import at.bitfire.dav4jvm.ktor.toUrlOrNull
import at.bitfire.dav4jvm.property.push.WebDAVPush
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.di.IoDispatcher
import at.bitfire.davdroid.network.HttpClientBuilder
import at.bitfire.davdroid.push.PushRegistrationManager.Companion.mutex
import at.bitfire.davdroid.repository.AccountRepository
@@ -36,7 +35,6 @@ import io.ktor.http.HttpHeaders
import io.ktor.http.Url
import io.ktor.http.isSuccess
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.unifiedpush.android.connector.UnifiedPush
@@ -63,46 +61,25 @@ class PushRegistrationManager @Inject constructor(
private val collectionRepository: DavCollectionRepository,
@ApplicationContext private val context: Context,
private val httpClientBuilder: Provider<HttpClientBuilder>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val logger: Logger,
private val serviceRepository: DavServiceRepository
private val serviceRepository: DavServiceRepository,
private val distributorManager: PushDistributorManager
) {
/**
* Sets or removes (disable push) the distributor and updates the subscriptions + worker.
*
* Uses [update] which is protected by [mutex] so creating/deleting subscriptions doesn't
* interfere with other operations.
*
* @param pushDistributor new distributor or `null` to disable Push
*/
suspend fun setPushDistributor(pushDistributor: String?) {
// Disable UnifiedPush and remove all subscriptions
UnifiedPush.removeDistributor(context)
update()
if (pushDistributor != null) {
// If a distributor was passed, store it and create/register subscriptions
UnifiedPush.saveDistributor(context, pushDistributor)
update()
}
}
fun getCurrentDistributor() = UnifiedPush.getSavedDistributor(context)
fun getDistributors() = UnifiedPush.getDistributors(context)
/**
* Updates all push registrations and subscriptions so that if Push is available, it's up-to-date and
* working for all database services. If Push is not available, existing subscriptions are unregistered.
*
* Also makes sure that the [PushRegistrationWorker] is enabled if there's a Push-enabled collection.
*
* Selects the distributor if none is selected yet and Push is not explicitly disabled in settings through the [PushDistributorManager].
*
* Acquires [mutex] so that this method can't be called twice at the same time, or at the same time
* with [update(serviceId)].
*/
suspend fun update() = mutex.withLock {
distributorManager.update()
for (service in serviceRepository.getAll())
updateService(service.id)
@@ -116,6 +93,7 @@ class PushRegistrationManager @Inject constructor(
* as [update()].
*/
suspend fun update(serviceId: Long) = mutex.withLock {
distributorManager.update()
updateService(serviceId)
updatePeriodicWorker()
}
@@ -129,7 +107,7 @@ class PushRegistrationManager @Inject constructor(
// use service ID from database as UnifiedPush instance name
val instance = serviceId.toString()
val distributorAvailable = getCurrentDistributor() != null
val distributorAvailable = distributorManager.getCurrentDistributor() != null
if (distributorAvailable)
try {
val vapid = collectionRepository.getVapidKey(serviceId)
@@ -353,6 +331,19 @@ class PushRegistrationManager @Inject constructor(
}
/**
* Allows preferring certain distributors over others.
*/
interface DistributorPreferences {
/**
* A list of package names ordered by preference.
* If any of those is available, it will be automatically selected.
* Otherwise, another available distributor will be chosen automatically.
*/
val packageNames: List<String>
}
companion object {
private const val WORKER_UNIQUE_NAME = "push-registration"

View File

@@ -65,5 +65,12 @@ object Settings {
/** max. number of accounts */
const val MAX_ACCOUNTS = "max_accounts"
/**
* By default, a push distributor is automatically selected when needed. However, the user can choose to disable push completely.
* This setting reflects that choice.
*/
const val EXPLICIT_PUSH_DISABLE = "push_disable"
}

View File

@@ -15,6 +15,7 @@ import androidx.lifecycle.viewModelScope
import at.bitfire.cert4android.CustomCertStore
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.di.IoDispatcher
import at.bitfire.davdroid.push.PushDistributorManager
import at.bitfire.davdroid.push.PushRegistrationManager
import at.bitfire.davdroid.repository.PreferenceRepository
import at.bitfire.davdroid.settings.Settings
@@ -43,6 +44,7 @@ class AppSettingsModel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val preferences: PreferenceRepository,
private val pushRegistrationManager: PushRegistrationManager,
private val pushDistributorManager: PushDistributorManager,
private val settings: SettingsManager,
tasksAppManager: TasksAppManager
) : ViewModel() {
@@ -138,10 +140,10 @@ class AppSettingsModel @Inject constructor(
* - Makes sure the app is registered with UnifiedPush if there's already a distributor selected.
*/
private fun loadPushDistributors() {
val currentPushDistributor = pushRegistrationManager.getCurrentDistributor()
val currentPushDistributor = pushDistributorManager.getCurrentDistributor()
_pushDistributor.value = currentPushDistributor
val pushDistributors = pushRegistrationManager.getDistributors()
val pushDistributors = pushDistributorManager.getDistributors()
.map { pushDistributor ->
try {
val applicationInfo = pm.getApplicationInfo(pushDistributor, 0)
@@ -165,7 +167,14 @@ class AppSettingsModel @Inject constructor(
*/
fun updatePushDistributor(pushDistributor: String?) {
viewModelScope.launch(ioDispatcher) {
pushRegistrationManager.setPushDistributor(pushDistributor)
pushDistributorManager.setPushDistributor(pushDistributor)
if (pushDistributor == null) {
// Disable push explicitly, this will disable the automatic distributor selector
settings.putBoolean(Settings.EXPLICIT_PUSH_DISABLE, true)
} else {
settings.remove(Settings.EXPLICIT_PUSH_DISABLE)
}
_pushDistributor.value = pushDistributor
}

View File

@@ -4,6 +4,8 @@
package at.bitfire.davdroid.di
import at.bitfire.davdroid.push.DistributorPreferencesProvider
import at.bitfire.davdroid.push.PushRegistrationManager
import at.bitfire.davdroid.ui.AccountsDrawerHandler
import at.bitfire.davdroid.ui.OseAccountsDrawerHandler
import at.bitfire.davdroid.ui.about.AboutActivity
@@ -46,6 +48,9 @@ interface OseModules {
interface Global {
@Binds
fun introPageFactory(impl: OseIntroPageFactory): IntroPageFactory
@Binds
fun pushDistributorPreferences(impl: DistributorPreferencesProvider): PushRegistrationManager.DistributorPreferences
}
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.push
import javax.inject.Inject
class DistributorPreferencesProvider @Inject constructor() : PushRegistrationManager.DistributorPreferences {
// No special preferences for OSE flavor, select the first distributor available
override val packageNames: List<String> = emptyList()
}