From bcc16e1ab6b7c35f2bdf4bc1b73df3c7726b0cf7 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 25 Jun 2024 14:07:29 +0200 Subject: [PATCH] Implement basic Push functionality (#856) * Move PushRegistrationWorker to push package * Add UP dependency * [WIP] UnifiedPush basic implementation * Handle endpoint unregistration * [WIP] Parse push notification message * Parse push message in PushMessageParser * Sync only affected account on push message * Only initiate sync when push message is about a syncable collection * Push registration worker: log when there's no configured endpoint * Handle invalid/non-XML push messages * app settings: show UP endpoint --- app/build.gradle.kts | 1 + .../kotlin/at/bitfire/davdroid/TestModules.kt | 2 +- app/src/main/AndroidManifest.xml | 10 +++ .../at/bitfire/davdroid/db/CollectionDao.kt | 3 + .../at/bitfire/davdroid/db/HomeSetDao.kt | 1 - .../davdroid/push/PushMessageParser.kt | 40 ++++++++++ .../PushRegistrationWorker.kt | 26 +++--- .../davdroid/push/UnifiedPushReceiver.kt | 79 +++++++++++++++++++ .../davdroid/repository/AccountRepository.kt | 15 ++-- .../repository/DavCollectionRepository.kt | 4 +- .../repository/PreferenceRepository.kt | 18 +++++ .../bitfire/davdroid/ui/AppSettingsModel.kt | 4 + .../bitfire/davdroid/ui/AppSettingsScreen.kt | 19 ++++- .../at/bitfire/davdroid/ui/TasksModel.kt | 2 +- .../davdroid/ui/composable/BasicTopAppBar.kt | 1 - .../ui/composable/ExceptionInfoDialog.kt | 1 - .../ui/composable/PermissionSwitchRow.kt | 1 - .../ui/intro/BatteryOptimizationsPage.kt | 2 +- .../intro/BatteryOptimizationsPageContent.kt | 2 +- .../ui/intro/BatteryOptimizationsPageModel.kt | 2 +- .../davdroid/ui/intro/PermissionsIntroPage.kt | 1 - app/src/main/res/values/strings.xml | 2 + .../davdroid/push/PushMessageParserTest.kt | 31 ++++++++ gradle/libs.versions.toml | 4 +- 24 files changed, 242 insertions(+), 29 deletions(-) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageParser.kt rename app/src/main/kotlin/at/bitfire/davdroid/{syncadapter => push}/PushRegistrationWorker.kt (86%) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushReceiver.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/push/PushMessageParserTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bf1ea04a..367818cd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,6 +198,7 @@ dependencies { implementation(libs.okhttp.brotli) implementation(libs.okhttp.logging) implementation(libs.openid.appauth) + implementation(libs.unifiedpush) // for tests androidTestImplementation(libs.androidx.arch.core.testing) diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/TestModules.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/TestModules.kt index e88fd08b..e8fc487c 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/TestModules.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/TestModules.kt @@ -1,7 +1,7 @@ package at.bitfire.davdroid import at.bitfire.davdroid.repository.DavCollectionRepository -import at.bitfire.davdroid.syncadapter.PushRegistrationWorker +import at.bitfire.davdroid.push.PushRegistrationWorker import dagger.Module import dagger.hilt.components.SingletonComponent import dagger.multibindings.Multibinds diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 76343e70..4e0e874a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -288,6 +288,16 @@ android:resource="@xml/debug_paths" /> + + + + + + + + + + diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt index 6216783d..47ebb6d6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt @@ -32,6 +32,9 @@ interface CollectionDao { @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE") fun getByServiceAndType(serviceId: Long, type: String): List + @Query("SELECT * FROM collection WHERE pushTopic=:topic AND sync") + fun getSyncableByPushTopic(topic: String): Collection? + @Query("SELECT COUNT(*) FROM collection WHERE serviceId=:serviceId AND type=:type") suspend fun anyOfType(serviceId: Long, type: String): Boolean diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt index 57523028..55ec317c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt @@ -8,7 +8,6 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query -import androidx.room.Transaction import androidx.room.Update import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageParser.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageParser.kt new file mode 100644 index 00000000..2354cd22 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageParser.kt @@ -0,0 +1,40 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import at.bitfire.dav4jvm.XmlUtils +import at.bitfire.dav4jvm.property.push.PushMessage +import at.bitfire.davdroid.log.Logger +import org.xmlpull.v1.XmlPullParserException +import java.io.StringReader +import java.util.logging.Level +import javax.inject.Inject + +class PushMessageParser @Inject constructor() { + + /** + * Parses a WebDAV-Push message and returns the `topic` that the message is about. + * + * @return topic of the modified collection, or `null` if the topic couldn't be determined + */ + operator fun invoke(message: String): String? { + var topic: String? = null + + val parser = XmlUtils.newPullParser() + try { + parser.setInput(StringReader(message)) + + XmlUtils.processTag(parser, PushMessage.NAME) { + val pushMessage = PushMessage.Factory.create(parser) + topic = pushMessage.topic + } + } catch (e: XmlPullParserException) { + Logger.log.log(Level.WARNING, "Couldn't parse push message", e) + } + + return topic + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/PushRegistrationWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt similarity index 86% rename from app/src/main/kotlin/at/bitfire/davdroid/syncadapter/PushRegistrationWorker.kt rename to app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt index 80f17e5b..390c9663 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/PushRegistrationWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt @@ -1,4 +1,8 @@ -package at.bitfire.davdroid.syncadapter +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push import android.accounts.Account import android.content.Context @@ -22,6 +26,7 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.repository.PreferenceRepository import at.bitfire.davdroid.settings.AccountSettings import dagger.Binds import dagger.Module @@ -51,6 +56,7 @@ class PushRegistrationWorker @AssistedInject constructor( @Assisted context: Context, @Assisted workerParameters: WorkerParameters, private val collectionRepository: DavCollectionRepository, + private val preferenceRepository: PreferenceRepository, private val serviceRepository: DavServiceRepository ) : CoroutineWorker(context, workerParameters) { @@ -118,16 +124,18 @@ class PushRegistrationWorker @AssistedInject constructor( override suspend fun doWork(): Result { Logger.log.info("Running push registration worker") - // We will get this endpoint from UnifiedPush: - val sampleEndpoint = "https://endpoint.example.com" + val endpoint = preferenceRepository.unifiedPushEndpoint() - for (collection in collectionRepository.getSyncEnabledAndPushCapable()) { - Logger.log.info("Registering push for ${collection.url}") - val service = serviceRepository.get(collection.serviceId) ?: continue - val account = Account(service.accountName, applicationContext.getString(R.string.account_type)) + if (endpoint != null) + for (collection in collectionRepository.getSyncableAndPushCapable()) { + Logger.log.info("Registering push for ${collection.url}") + val service = serviceRepository.get(collection.serviceId) ?: continue + val account = Account(service.accountName, applicationContext.getString(R.string.account_type)) - requestPushRegistration(collection, account, sampleEndpoint) - } + requestPushRegistration(collection, account, endpoint) + } + else + Logger.log.info("No UnifiedPush endpoint configured") return Result.success() } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushReceiver.kt new file mode 100644 index 00000000..301a2435 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushReceiver.kt @@ -0,0 +1,79 @@ +/* + * 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.log.Logger +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.repository.PreferenceRepository +import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker +import dagger.hilt.android.AndroidEntryPoint +import org.unifiedpush.android.connector.MessagingReceiver +import java.util.logging.Level +import javax.inject.Inject + +@AndroidEntryPoint +class UnifiedPushReceiver: MessagingReceiver() { + + @Inject + lateinit var accountRepository: AccountRepository + + @Inject + lateinit var collectionRepository: DavCollectionRepository + + @Inject + lateinit var serviceRepository: DavServiceRepository + + @Inject + lateinit var preferenceRepository: PreferenceRepository + + @Inject + lateinit var parsePushMessage: PushMessageParser + + + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + // remember new endpoint + preferenceRepository.unifiedPushEndpoint(endpoint) + + // register new endpoint at CalDAV/CardDAV servers + PushRegistrationWorker.enqueue(context) + } + + override fun onUnregistered(context: Context, instance: String) { + // reset known endpoint + preferenceRepository.unifiedPushEndpoint(null) + } + + override fun onMessage(context: Context, message: ByteArray, instance: String) { + val messageXml = message.toString(Charsets.UTF_8) + Logger.log.log(Level.INFO, "Received push message", messageXml) + + // parse push notification + val topic = parsePushMessage(messageXml) + + // sync affected collection + if (topic != null) { + Logger.log.info("Got push notification for topic $topic") + + // Sync all authorities of account that the collection belongs to + // Later: only sync affected collection and authorities + collectionRepository.getSyncableByTopic(topic)?.let { collection -> + serviceRepository.get(collection.serviceId)?.let { service -> + val account = accountRepository.fromName(service.accountName) + OneTimeSyncWorker.enqueueAllAuthorities(context, account) + } + } + + } else { + Logger.log.warning("Got push message without topic, syncing all accounts") + for (account in accountRepository.getAll()) + OneTimeSyncWorker.enqueueAllAuthorities(context, account) + + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt index ef7987b5..16d4dc03 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt @@ -69,7 +69,7 @@ class AccountRepository @Inject constructor( * @return account if account creation was successful; null otherwise (for instance because an account with this name already exists) */ fun create(accountName: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): Account? { - val account = account(accountName) + val account = fromName(accountName) // create Android account val userData = AccountSettings.initialUserData(credentials) @@ -135,7 +135,7 @@ class AccountRepository @Inject constructor( suspend fun delete(accountName: String): Boolean { // remove account - val future = accountManager.removeAccount(account(accountName), null, null, null) + val future = accountManager.removeAccount(fromName(accountName), null, null, null) return try { // wait for operation to complete withContext(Dispatchers.Default) { @@ -162,7 +162,10 @@ class AccountRepository @Inject constructor( else accountManager .getAccountsByType(accountType) - .contains(Account(accountName, accountType)) + .any { it.name == accountName } + + fun fromName(accountName: String) = + Account(accountName, accountType) fun getAll(): Array = accountManager.getAccountsByType(accountType) @@ -191,8 +194,8 @@ class AccountRepository @Inject constructor( * @throws Exception (or sub-classes) on other errors */ suspend fun rename(oldName: String, newName: String) { - val oldAccount = account(oldName) - val newAccount = account(newName) + val oldAccount = fromName(oldName) + val newAccount = fromName(newName) // check whether new account name already exists if (accountManager.getAccountsByType(context.getString(R.string.account_type)).contains(newAccount)) @@ -286,8 +289,6 @@ class AccountRepository @Inject constructor( // helpers - private fun account(accountName: String) = Account(accountName, accountType) - private fun insertService(accountName: String, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { // insert service val service = Service(0, accountName, type, info.principal) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt index 52531146..2d27e6f0 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt @@ -179,10 +179,12 @@ class DavCollectionRepository @Inject constructor( } } + fun getSyncableByTopic(topic: String) = dao.getSyncableByPushTopic(topic) + fun getFlow(id: Long) = dao.getFlow(id) /** Returns all collections that are both selected for synchronization and push-capable. */ - suspend fun getSyncEnabledAndPushCapable(): List = + suspend fun getSyncableAndPushCapable(): List = dao.getPushCapableSyncCollections() /** diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt index 177273d1..3b029065 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt @@ -22,6 +22,10 @@ class PreferenceRepository @Inject constructor( context: Application ) { + companion object { + const val UNIFIED_PUSH_ENDPOINT = "unified_push_endpoint" + } + private val preferences = PreferenceManager.getDefaultSharedPreferences(context) /** @@ -41,6 +45,20 @@ class PreferenceRepository @Inject constructor( preferences.getBoolean(Logger.LOG_TO_FILE, false) } + fun unifiedPushEndpoint() = + preferences.getString(UNIFIED_PUSH_ENDPOINT, null) + + fun unifiedPushEndpointFlow() = observeAsFlow(UNIFIED_PUSH_ENDPOINT) { + unifiedPushEndpoint() + } + + fun unifiedPushEndpoint(endpoint: String?) { + preferences + .edit() + .putString(UNIFIED_PUSH_ENDPOINT, endpoint) + .apply() + } + private fun observeAsFlow(keyToObserve: String, getValue: () -> T): Flow = callbackFlow { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt index 2d143fca..2aaf3fe9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt @@ -101,4 +101,8 @@ class AppSettingsModel @Inject constructor( val icon = appInfoFlow.map { it?.loadIcon(pm) } + // push + + val pushEndpoint = preference.unifiedPushEndpointFlow() + } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt index e66f6fff..f049ec40 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource @@ -50,6 +51,7 @@ import at.bitfire.davdroid.ui.composable.Setting import at.bitfire.davdroid.ui.composable.SettingsHeader import at.bitfire.davdroid.ui.composable.SwitchSetting import kotlinx.coroutines.launch +import org.unifiedpush.android.connector.UnifiedPush @Composable fun AppSettingsScreen( @@ -92,9 +94,10 @@ fun AppSettingsScreen( onThemeSelected = model::updateTheme, onResetHints = model::resetHints, - // Integration (Tasks) + // Integration (Tasks and Push) tasksAppName = model.appName.collectAsStateWithLifecycle(null).value ?: stringResource(R.string.app_settings_tasks_provider_none), tasksAppIcon = model.icon.collectAsStateWithLifecycle(null).value, + pushEndpoint = model.pushEndpoint.collectAsStateWithLifecycle(null).value, onNavTasksScreen = onNavTasksScreen ) } @@ -133,6 +136,7 @@ fun AppSettingsScreen( // AppSettings Integration tasksAppName: String, tasksAppIcon: Drawable?, + pushEndpoint: String?, onNavTasksScreen: () -> Unit, onShowNotificationSettings: () -> Unit, @@ -219,6 +223,7 @@ fun AppSettingsScreen( AppSettings_Integration( appName = tasksAppName, icon = tasksAppIcon, + pushEndpoint = pushEndpoint, onNavTasksScreen = onNavTasksScreen ) } @@ -254,6 +259,7 @@ fun AppSettingsScreen_Preview() { onResetHints = {}, tasksAppName = "No tasks app", tasksAppIcon = null, + pushEndpoint = null, onNavTasksScreen = {} ) } @@ -464,6 +470,7 @@ fun AppSettings_UserInterface( @Composable fun AppSettings_Integration( appName: String, + pushEndpoint: String?, icon: Drawable? = null, onNavTasksScreen: () -> Unit = {} ) { @@ -482,4 +489,14 @@ fun AppSettings_Integration( summary = appName, onClick = onNavTasksScreen ) + + val context = LocalContext.current + + Setting( + name = "UnifiedPush", + summary = pushEndpoint ?: stringResource(R.string.app_settings_unifiedpush_no_endpoint), + onClick = { + UnifiedPush.registerAppWithDialog(context) + } + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt index 483ca531..dd6f879f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt @@ -12,10 +12,10 @@ import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.util.packageChangedFlow import at.bitfire.ical4android.TaskProvider import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class TasksModel @Inject constructor( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt index 15e5649a..142ae7a4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt @@ -5,7 +5,6 @@ package at.bitfire.davdroid.ui.composable import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt index 05e47ba6..03446406 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt index 8a1af4f5..ad6a6f5c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch -import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt index 1ec48645..1f6be1f6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt @@ -12,9 +12,9 @@ import android.net.Uri import androidx.activity.result.contract.ActivityResultContract import androidx.compose.runtime.Composable import at.bitfire.davdroid.settings.SettingsManager -import javax.inject.Inject import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel.Companion.HINT_AUTOSTART_PERMISSION import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel.Companion.HINT_BATTERY_OPTIMIZATIONS +import javax.inject.Inject class BatteryOptimizationsPage @Inject constructor( private val application: Application, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt index e4b41ec5..fdaf8b1d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt @@ -35,8 +35,8 @@ import at.bitfire.davdroid.Constants import at.bitfire.davdroid.Constants.withStatParams import at.bitfire.davdroid.R import at.bitfire.davdroid.ui.AppTheme -import java.util.Locale import org.apache.commons.text.WordUtils +import java.util.Locale @Composable fun BatteryOptimizationsPageContent( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt index 09d5aa83..bb81d8c1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt @@ -16,9 +16,9 @@ import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.util.broadcastReceiverFlow import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject -import kotlinx.coroutines.launch @HiltViewModel class BatteryOptimizationsPageModel @Inject constructor( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt index d4b7aac6..b0600817 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui.intro import android.app.Application import androidx.compose.runtime.Composable -import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.davdroid.ui.PermissionsModel import at.bitfire.davdroid.ui.PermissionsScreen import at.bitfire.davdroid.util.PermissionUtils diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cfa4265c..bd022e7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -209,6 +209,8 @@ Integration Tasks app No compatible tasks app found + UnifiedPush + No endpoint configured CardDAV diff --git a/app/src/test/kotlin/at/bitfire/davdroid/push/PushMessageParserTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/push/PushMessageParserTest.kt new file mode 100644 index 00000000..0a476636 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/push/PushMessageParserTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class PushMessageParserTest { + + private val parse = PushMessageParser() + + @Test + fun testInvalidXml() { + assertNull(parse("Non-XML content")) + } + + @Test + fun testWithXmlDeclAndTopic() { + val topic = parse( + "" + + "" + + "sample-topic" + + "" + ) + assertEquals("sample-topic", topic) + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe050749..0e97b836 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ androidx-test-junit = "1.1.5" androidx-work = "2.9.0" appIntro = "7.0.0-beta02" bitfire-cert4android = "f1cc9b9ca3" -bitfire-dav4jvm = "fa173ab215" +bitfire-dav4jvm = "b8be778202" bitfire-ical4android = "ba5a013d69" bitfire-vcard4android = "03a37a8284" commons-collections = "4.4" @@ -48,6 +48,7 @@ mockk = "1.13.11" okhttp = "4.12.0" openid-appauth = "0.11.1" room = "2.6.1" +unifiedpush = "2.4.0" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugaring" } @@ -110,6 +111,7 @@ room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-paging = { module = "androidx.room:room-paging", version.ref = "room" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-testing = { module = "androidx.room:room-testing", version.ref = "room" } +unifiedpush = { module = "com.github.UnifiedPush:android-connector", version.ref = "unifiedpush" } [plugins] android-application = { id = "com.android.application", version.ref = "android-agp" }