mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-01 07:14:36 +00:00
Compare commits
16 Commits
16cc3d0cb5
...
ccbe1d7645
Author | SHA1 | Date | |
---|---|---|---|
|
ccbe1d7645 | ||
|
85b29329eb | ||
|
c7b18b1f41 | ||
|
c2cca01b3b | ||
|
3027feb18d | ||
|
d44cfdf437 | ||
|
0935bca1a2 | ||
|
3aa205c3c3 | ||
|
1550c41a18 | ||
|
8d54e618fc | ||
|
05e708eed5 | ||
|
de1ceb625e | ||
|
e2ee0f459a | ||
|
ea035fa931 | ||
|
8167e8e3cb | ||
|
bcc16e1ab6 |
|
@ -18,8 +18,8 @@ android {
|
|||
defaultConfig {
|
||||
applicationId = "at.bitfire.davdroid"
|
||||
|
||||
versionCode = 404010000
|
||||
versionName = "4.4.1-alpha.1"
|
||||
versionCode = 404010001
|
||||
versionName = "4.4.1-alpha.2"
|
||||
|
||||
buildConfigField("long", "buildTime", "${System.currentTimeMillis()}L")
|
||||
|
||||
|
@ -184,7 +184,6 @@ dependencies {
|
|||
implementation(libs.bitfire.vcard4android)
|
||||
|
||||
// third-party libs
|
||||
implementation(libs.appintro)
|
||||
implementation(libs.commons.collections)
|
||||
@Suppress("RedundantSuppression")
|
||||
implementation(libs.commons.io)
|
||||
|
@ -198,6 +197,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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
|
||||
android:resizeableActivity="true"
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:supportsRtl="true">
|
||||
|
@ -288,6 +288,16 @@
|
|||
android:resource="@xml/debug_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- UnifiedPush receiver -->
|
||||
<receiver android:exported="true" android:enabled="true" android:name=".push.UnifiedPushReceiver" tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
|
||||
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
|
||||
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Widgets -->
|
||||
<receiver android:name=".ui.widget.SyncButtonWidgetReceiver"
|
||||
android:exported="true">
|
||||
|
|
|
@ -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<Collection>
|
||||
|
||||
@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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Account> = 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)
|
||||
|
|
|
@ -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<Collection> =
|
||||
suspend fun getSyncableAndPushCapable(): List<Collection> =
|
||||
dao.getPushCapableSyncCollections()
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<T> observeAsFlow(keyToObserve: String, getValue: () -> T): Flow<T> =
|
||||
callbackFlow {
|
||||
|
|
|
@ -101,4 +101,8 @@ class AppSettingsModel @Inject constructor(
|
|||
val icon = appInfoFlow.map { it?.loadIcon(pm) }
|
||||
|
||||
|
||||
// push
|
||||
|
||||
val pushEndpoint = preference.unifiedPushEndpointFlow()
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -8,98 +8,54 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import at.bitfire.davdroid.ui.AppTheme
|
||||
import at.bitfire.davdroid.ui.M3ColorScheme
|
||||
import com.github.appintro.AppIntro2
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class IntroActivity : AppIntro2() {
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
class IntroActivity : AppCompatActivity() {
|
||||
|
||||
val model by viewModels<Model>()
|
||||
private var currentSlide = 0
|
||||
val model by viewModels<IntroModel>()
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
model.pages.forEachIndexed { idx, _ ->
|
||||
addSlide(PageFragment().apply {
|
||||
arguments = Bundle(1).apply {
|
||||
putInt(PageFragment.ARG_PAGE_IDX, idx)
|
||||
}
|
||||
})
|
||||
}
|
||||
val pages = model.pages
|
||||
|
||||
setBarColor(M3ColorScheme.primaryLight.toArgb())
|
||||
isSkipButtonEnabled = false
|
||||
setContent {
|
||||
AppTheme {
|
||||
val scope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState { pages.size }
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (currentSlide == 0) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else {
|
||||
goToPreviousSlide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
currentSlide = position
|
||||
}
|
||||
|
||||
override fun onDonePressed(currentFragment: Fragment?) {
|
||||
super.onDonePressed(currentFragment)
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PageFragment: Fragment() {
|
||||
|
||||
companion object {
|
||||
const val ARG_PAGE_IDX = "page"
|
||||
}
|
||||
|
||||
val model by activityViewModels<Model>()
|
||||
val page by lazy { model.pages[requireArguments().getInt(ARG_PAGE_IDX)] }
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
|
||||
ComposeView(requireActivity()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.padding(bottom = dimensionResource(com.github.appintro.R.dimen.appintro2_bottombar_height))) {
|
||||
page.ComposePage()
|
||||
}
|
||||
}
|
||||
BackHandler {
|
||||
if (pagerState.settledPage == 0) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else scope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.settledPage - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IntroScreen(
|
||||
pages = pages,
|
||||
pagerState = pagerState,
|
||||
onDonePressed = {
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -115,49 +71,4 @@ class IntroActivity : AppIntro2() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
introPageFactory: IntroPageFactory
|
||||
): ViewModel() {
|
||||
|
||||
private val introPages = introPageFactory.introPages
|
||||
|
||||
private var _pages: List<IntroPage>? = null
|
||||
val pages: List<IntroPage>
|
||||
@Synchronized
|
||||
get() {
|
||||
_pages?.let { return it }
|
||||
|
||||
val newPages = calculatePages()
|
||||
_pages = newPages
|
||||
|
||||
return newPages
|
||||
}
|
||||
|
||||
private fun calculatePages(): List<IntroPage> {
|
||||
for (page in introPages)
|
||||
Logger.log.fine("Found intro page ${page::class.java} with order ${page.getShowPolicy()}")
|
||||
|
||||
val activePages: Map<IntroPage, IntroPage.ShowPolicy> = introPages
|
||||
.associateWith { page ->
|
||||
page.getShowPolicy().also { policy ->
|
||||
Logger.log.fine("IntroActivity: found intro page ${page::class.java} with $policy")
|
||||
}
|
||||
}
|
||||
.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }
|
||||
|
||||
val anyShowAlways = activePages.values.any { it == IntroPage.ShowPolicy.SHOW_ALWAYS }
|
||||
return if (anyShowAlways) {
|
||||
val pages = mutableListOf<IntroPage>()
|
||||
activePages.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }.forEach { page, _ ->
|
||||
pages += page
|
||||
}
|
||||
pages
|
||||
} else
|
||||
emptyList()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package at.bitfire.davdroid.ui.intro
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class IntroModel @Inject constructor(
|
||||
introPageFactory: IntroPageFactory
|
||||
): ViewModel() {
|
||||
|
||||
private val introPages = introPageFactory.introPages
|
||||
|
||||
val pages: List<IntroPage> by lazy {
|
||||
calculatePages()
|
||||
}
|
||||
|
||||
|
||||
private fun calculatePages(): List<IntroPage> {
|
||||
for (page in introPages)
|
||||
Logger.log.fine("Found intro page ${page::class.java} with order ${page.getShowPolicy()}")
|
||||
|
||||
// Calculate which intro pages shall be shown
|
||||
val activePages: Map<IntroPage, IntroPage.ShowPolicy> = introPages
|
||||
.associateWith { page ->
|
||||
page.getShowPolicy().also { policy ->
|
||||
Logger.log.fine("IntroActivity: found intro page ${page::class.java} with $policy")
|
||||
}
|
||||
}
|
||||
.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }
|
||||
|
||||
// Show intro screen when there's at least one page that shall [always] be shown
|
||||
val anyShowAlways = activePages.values.any { it == IntroPage.ShowPolicy.SHOW_ALWAYS }
|
||||
return if (anyShowAlways) {
|
||||
val pages = mutableListOf<IntroPage>()
|
||||
activePages.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }.forEach { page, _ ->
|
||||
pages += page
|
||||
}
|
||||
pages
|
||||
} else
|
||||
emptyList()
|
||||
}
|
||||
|
||||
}
|
255
app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt
Normal file
255
app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt
Normal file
|
@ -0,0 +1,255 @@
|
|||
package at.bitfire.davdroid.ui.intro
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.ui.AppTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun IntroScreen(
|
||||
pages: List<IntroPage>,
|
||||
pagerState: PagerState = rememberPagerState { pages.size },
|
||||
onDonePressed: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) { pages[it].ComposePage() }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(90.dp)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
PositionIndicator(
|
||||
index = pagerState.currentPage,
|
||||
max = pages.size,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 128.dp)
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth(),
|
||||
selectedIndicatorColor = MaterialTheme.colorScheme.onPrimary,
|
||||
unselectedIndicatorColor = MaterialTheme.colorScheme.tertiary,
|
||||
indicatorSize = 15f
|
||||
)
|
||||
|
||||
ButtonWithIcon(
|
||||
icon = if (pagerState.currentPage + 1 == pagerState.pageCount) {
|
||||
Icons.Default.Check
|
||||
} else {
|
||||
Icons.AutoMirrored.Default.ArrowForward
|
||||
},
|
||||
contentDescription = stringResource(R.string.intro_next),
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
) {
|
||||
if (pagerState.currentPage + 1 == pagerState.pageCount) {
|
||||
onDonePressed()
|
||||
} else scope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
showSystemUi = true
|
||||
)
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun IntroScreen_Preview() {
|
||||
AppTheme {
|
||||
IntroScreen(
|
||||
listOf(
|
||||
object : IntroPage {
|
||||
override fun getShowPolicy(): IntroPage.ShowPolicy =
|
||||
IntroPage.ShowPolicy.SHOW_ALWAYS
|
||||
|
||||
@Composable
|
||||
override fun ComposePage() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
)
|
||||
}
|
||||
},
|
||||
object : IntroPage {
|
||||
override fun getShowPolicy(): IntroPage.ShowPolicy =
|
||||
IntroPage.ShowPolicy.SHOW_ALWAYS
|
||||
|
||||
@Composable
|
||||
override fun ComposePage() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
onDonePressed = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun PositionIndicator(
|
||||
index: Int,
|
||||
max: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
selectedIndicatorColor: Color = MaterialTheme.colorScheme.tertiary,
|
||||
unselectedIndicatorColor: Color = contentColorFor(selectedIndicatorColor),
|
||||
indicatorSize: Float = 20f,
|
||||
indicatorPadding: Float = 20f
|
||||
) {
|
||||
val selectedPosition by animateFloatAsState(
|
||||
targetValue = index.toFloat(),
|
||||
label = "position"
|
||||
)
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
// idx * indicatorSize * 2 + idx * indicatorPadding + indicatorSize
|
||||
// idx * (indicatorSize * 2 + indicatorPadding) + indicatorSize
|
||||
val padding = indicatorSize * 2 + indicatorPadding
|
||||
|
||||
val totalWidth = indicatorSize * 2 * max + indicatorPadding * (max - 1)
|
||||
translate(
|
||||
left = size.width / 2 - totalWidth / 2
|
||||
) {
|
||||
for (idx in 0 until max) {
|
||||
drawCircle(
|
||||
color = unselectedIndicatorColor,
|
||||
radius = indicatorSize,
|
||||
center = Offset(
|
||||
x = idx * padding + indicatorSize,
|
||||
y = size.height / 2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
drawCircle(
|
||||
color = selectedIndicatorColor,
|
||||
radius = indicatorSize,
|
||||
center = Offset(
|
||||
x = selectedPosition * padding + indicatorSize,
|
||||
y = size.height / 2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
showBackground = true,
|
||||
backgroundColor = 0xff000000
|
||||
)
|
||||
@Composable
|
||||
fun PositionIndicator_Preview() {
|
||||
var index by remember { mutableIntStateOf(0) }
|
||||
|
||||
PositionIndicator(
|
||||
index = index,
|
||||
max = 5,
|
||||
modifier = Modifier
|
||||
.width(200.dp)
|
||||
.height(50.dp)
|
||||
.clickable { if (index == 4) index = 0 else index++ }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ButtonWithIcon(
|
||||
icon: ImageVector,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 56.dp,
|
||||
color: Color = MaterialTheme.colorScheme.tertiary,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
color = color,
|
||||
contentColor = contentColorFor(backgroundColor = color),
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.aspectRatio(1f),
|
||||
onClick = onClick,
|
||||
shape = CircleShape
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = icon,
|
||||
label = "Button Icon"
|
||||
) {
|
||||
Icon(
|
||||
imageVector = it,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ButtonWithIcon_Preview() {
|
||||
AppTheme {
|
||||
ButtonWithIcon(
|
||||
icon = Icons.AutoMirrored.Filled.ArrowForward,
|
||||
contentDescription = null
|
||||
) { }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -22,7 +22,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -84,11 +83,6 @@ class WelcomePage: IntroPage {
|
|||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(
|
||||
bottom = dimensionResource(
|
||||
com.github.appintro.R.dimen.appintro2_bottombar_height
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f))
|
||||
|
@ -128,12 +122,7 @@ class WelcomePage: IntroPage {
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.primary)
|
||||
.padding(
|
||||
bottom = dimensionResource(
|
||||
com.github.appintro.R.dimen.appintro2_bottombar_height
|
||||
)
|
||||
),
|
||||
.background(color = MaterialTheme.colorScheme.primary),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
<string name="intro_open_source_text">We\'re happy that you use %s, which is open-source software. Development, maintenance and support are hard work. Please consider contributing (there are many ways) or a donation. It would be highly appreciated!</string>
|
||||
<string name="intro_open_source_details">How to contribute/donate</string>
|
||||
<string name="intro_open_source_dont_show">Don\'t show in the near future</string>
|
||||
<string name="intro_next">Next</string>
|
||||
|
||||
<!-- PermissionsActivity -->
|
||||
<string name="permissions_title">Permissions</string>
|
||||
|
@ -209,6 +210,8 @@
|
|||
<string name="app_settings_integration">Integration</string>
|
||||
<string name="app_settings_tasks_provider">Tasks app</string>
|
||||
<string name="app_settings_tasks_provider_none">No compatible tasks app found</string>
|
||||
<string name="app_settings_unifiedpush" translatable="false">UnifiedPush</string>
|
||||
<string name="app_settings_unifiedpush_no_endpoint">No endpoint configured</string>
|
||||
|
||||
<!-- AccountScreen -->
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<!--
|
||||
~ Copyright (c) 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||
~ All rights reserved. This program and the accompanying materials
|
||||
~ are made available under the terms of the GNU Public License v3.0
|
||||
~ which accompanies this distribution, and is available at
|
||||
~ http://www.gnu.org/licenses/gpl.html
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- app theme -->
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- These non-Compose theme variables are required for AppIntro as long it doesn't support Compose: -->
|
||||
<item name="colorPrimary">@color/primaryColor</item>
|
||||
<item name="colorPrimaryDark">@color/primaryDarkColor</item>
|
||||
<item name="android:colorBackground">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -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(
|
||||
"<?xml version=\"1.0\" ?>" +
|
||||
"<push-message xmlns='DAV:Push'>" +
|
||||
"<topic>sample-topic</topic>" +
|
||||
"</push-message>"
|
||||
)
|
||||
assertEquals("sample-topic", topic)
|
||||
}
|
||||
|
||||
}
|
|
@ -13,15 +13,14 @@ androidx-lifecycle = "2.8.2"
|
|||
androidx-paging = "3.3.0"
|
||||
androidx-preference = "1.2.1"
|
||||
androidx-security = "1.1.0-alpha06"
|
||||
androidx-test-core = "1.5.0"
|
||||
androidx-test-runner = "1.5.2"
|
||||
androidx-test-rules = "1.5.0"
|
||||
androidx-test-junit = "1.1.5"
|
||||
androidx-test-core = "1.6.0"
|
||||
androidx-test-runner = "1.6.0"
|
||||
androidx-test-rules = "1.6.0"
|
||||
androidx-test-junit = "1.2.0"
|
||||
androidx-work = "2.9.0"
|
||||
appIntro = "7.0.0-beta02"
|
||||
bitfire-cert4android = "f1cc9b9ca3"
|
||||
bitfire-dav4jvm = "fa173ab215"
|
||||
bitfire-ical4android = "ba5a013d69"
|
||||
bitfire-dav4jvm = "b8be778202"
|
||||
bitfire-ical4android = "83cda23ceb"
|
||||
bitfire-vcard4android = "03a37a8284"
|
||||
commons-collections = "4.4"
|
||||
commons-lang = "3.14.0"
|
||||
|
@ -48,6 +47,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" }
|
||||
|
@ -72,7 +72,6 @@ androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-
|
|||
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
|
||||
androidx-work-base = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
|
||||
androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
|
||||
appintro = { module = "com.github.AppIntro:AppIntro", version.ref = "appIntro" }
|
||||
bitfire-cert4android = { module = "com.github.bitfireAT:cert4android", version.ref = "bitfire-cert4android" }
|
||||
bitfire-dav4jvm = { module = "com.github.bitfireAT:dav4jvm", version.ref = "bitfire-dav4jvm" }
|
||||
bitfire-ical4android = { module = "com.github.bitfireAT:ical4android", version.ref = "bitfire-ical4android" }
|
||||
|
@ -110,6 +109,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" }
|
||||
|
|
Loading…
Reference in New Issue
Block a user